View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package javax.mail.internet;
21  
22  import java.io.BufferedOutputStream;
23  import java.io.ByteArrayInputStream;
24  import java.io.ByteArrayOutputStream;
25  import java.io.FileOutputStream;
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.io.UnsupportedEncodingException;
31  import java.util.Enumeration;
32  
33  import javax.activation.DataHandler;
34  import javax.activation.FileDataSource;
35  import javax.mail.BodyPart;
36  import javax.mail.MessagingException;
37  import javax.mail.Multipart;
38  import javax.mail.Part;
39  import javax.mail.internet.HeaderTokenizer.Token;
40  import javax.swing.text.AbstractDocument.Content;
41  
42  import org.apache.geronimo.mail.util.ASCIIUtil;
43  import org.apache.geronimo.mail.util.SessionUtil;
44  
45  /**
46   * @version $Rev: 689126 $ $Date: 2008-08-26 12:17:53 -0400 (Tue, 26 Aug 2008) $
47   */
48  public class MimeBodyPart extends BodyPart implements MimePart {
49  	 // constants for accessed properties
50      private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
51      private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
52      private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset";
53      private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename";
54  
55  
56      /**
57       * The {@link DataHandler} for this Message's content.
58       */
59      protected DataHandler dh;
60      /**
61       * This message's content (unless sourced from a SharedInputStream).
62       */
63      protected byte content[];
64      /**
65       * If the data for this message was supplied by a {@link SharedInputStream}
66       * then this is another such stream representing the content of this message;
67       * if this field is non-null, then {@link #content} will be null.
68       */
69      protected InputStream contentStream;
70      /**
71       * This message's headers.
72       */
73      protected InternetHeaders headers;
74  
75      public MimeBodyPart() {
76          headers = new InternetHeaders();
77      }
78  
79      public MimeBodyPart(InputStream in) throws MessagingException {
80          headers = new InternetHeaders(in);
81          ByteArrayOutputStream baos = new ByteArrayOutputStream();
82          byte[] buffer = new byte[1024];
83          int count;
84          try {
85              while((count = in.read(buffer, 0, 1024)) > 0) {
86                  baos.write(buffer, 0, count);
87              }
88          } catch (IOException e) {
89              throw new MessagingException(e.toString(),e);
90          }
91          content = baos.toByteArray();
92      }
93  
94      public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
95          this.headers = headers;
96          this.content = content;
97      }
98  
99      /**
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 }