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: 627556 $ $Date: 2008-02-13 13:27:22 -0500 (Wed, 13 Feb 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          } catch (IOException e) {
88              throw new MessagingException(e.toString(),e);
89          }
90          content = baos.toByteArray();
91      }
92  
93      public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
94          this.headers = headers;
95          this.content = content;
96      }
97  
98      /**
99       * 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         // there's an optional session property that requests file name encoding...we need to process this before
331         // setting the value.
332         if (name != null && SessionUtil.getBooleanProperty(MIME_ENCODEFILENAME, false)) {
333             try {
334                 name = MimeUtility.encodeText(name);
335             } catch (UnsupportedEncodingException e) {
336                 throw new MessagingException("Unable to encode filename", e);
337             }
338         }
339 
340         // get the disposition string.
341         String disposition = getDisposition();
342         // if not there, then this is an attachment.
343         if (disposition == null) {
344             disposition = Part.ATTACHMENT;
345         }
346 
347         // now create a disposition object and set the parameter.
348         ContentDisposition contentDisposition = new ContentDisposition(disposition);
349         contentDisposition.setParameter("filename", name);
350 
351         // serialize this back out and reset.
352         setHeader("Content-Disposition", contentDisposition.toString());
353 
354         // The Sun implementation appears to update the Content-type name parameter too, based on
355         // another system property
356         if (SessionUtil.getBooleanProperty(MIME_SETCONTENTTYPEFILENAME, true)) {
357             ContentType type = new ContentType(getContentType());
358             type.setParameter("name", name);
359             setHeader("Content-Type", type.toString());
360         }
361     }
362 
363     public InputStream getInputStream() throws MessagingException, IOException {
364         return getDataHandler().getInputStream();
365     }
366 
367     protected InputStream getContentStream() throws MessagingException {
368         if (contentStream != null) {
369             return contentStream;
370         }
371 
372         if (content != null) {
373             return new ByteArrayInputStream(content);
374         } else {
375             throw new MessagingException("No content");
376         }
377     }
378 
379     public InputStream getRawInputStream() throws MessagingException {
380         return getContentStream();
381     }
382 
383     public synchronized DataHandler getDataHandler() throws MessagingException {
384         if (dh == null) {
385             dh = new DataHandler(new MimePartDataSource(this));
386         }
387         return dh;
388     }
389 
390     public Object getContent() throws MessagingException, IOException {
391         return getDataHandler().getContent();
392     }
393 
394     public void setDataHandler(DataHandler handler) throws MessagingException {
395         dh = handler;
396         // if we have a handler override, then we need to invalidate any content
397         // headers that define the types.  This information will be derived from the
398         // data heander unless subsequently overridden.
399         removeHeader("Content-Type");
400         removeHeader("Content-Transfer-Encoding");
401 
402     }
403 
404     public void setContent(Object content, String type) throws MessagingException {
405         // Multipart content needs to be handled separately.
406         if (content instanceof Multipart) {
407             setContent((Multipart)content);
408         }
409         else {
410             setDataHandler(new DataHandler(content, type));
411         }
412 
413     }
414 
415     public void setText(String text) throws MessagingException {
416         setText(text, null);
417     }
418 
419     public void setText(String text, String charset) throws MessagingException {
420         // the default subtype is plain text.
421         setText(text, charset, "plain");
422     }
423 
424 
425     public void setText(String text, String charset, String subtype) throws MessagingException {
426         // we need to sort out the character set if one is not provided.
427         if (charset == null) {
428             // if we have non us-ascii characters here, we need to adjust this.
429             if (!ASCIIUtil.isAscii(text)) {
430                 charset = MimeUtility.getDefaultMIMECharset();
431             }
432             else {
433                 charset = "us-ascii";
434             }
435         }
436         setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME));
437     }
438 
439     public void setContent(Multipart part) throws MessagingException {
440         setDataHandler(new DataHandler(part, part.getContentType()));
441         part.setParent(this);
442     }
443 
444     public void writeTo(OutputStream out) throws IOException, MessagingException {
445         headers.writeTo(out, null);
446         // add the separater between the headers and the data portion.
447         out.write('\r');
448         out.write('\n');
449         // we need to process this using the transfer encoding type
450         OutputStream encodingStream = MimeUtility.encode(out, getEncoding());
451         getDataHandler().writeTo(encodingStream);
452         encodingStream.flush();
453     }
454 
455     public String[] getHeader(String name) throws MessagingException {
456         return headers.getHeader(name);
457     }
458 
459     public String getHeader(String name, String delimiter) throws MessagingException {
460         return headers.getHeader(name, delimiter);
461     }
462 
463     public void setHeader(String name, String value) throws MessagingException {
464         headers.setHeader(name, value);
465     }
466 
467     /**
468      * Conditionally set or remove a named header.  If the new value
469      * is null, the header is removed.
470      *
471      * @param name   The header name.
472      * @param value  The new header value.  A null value causes the header to be
473      *               removed.
474      *
475      * @exception MessagingException
476      */
477     private void setOrRemoveHeader(String name, String value) throws MessagingException {
478         if (value == null) {
479             headers.removeHeader(name);
480         }
481         else {
482             headers.setHeader(name, value);
483         }
484     }
485 
486     public void addHeader(String name, String value) throws MessagingException {
487         headers.addHeader(name, value);
488     }
489 
490     public void removeHeader(String name) throws MessagingException {
491         headers.removeHeader(name);
492     }
493 
494     public Enumeration getAllHeaders() throws MessagingException {
495         return headers.getAllHeaders();
496     }
497 
498     public Enumeration getMatchingHeaders(String[] name) throws MessagingException {
499         return headers.getMatchingHeaders(name);
500     }
501 
502     public Enumeration getNonMatchingHeaders(String[] name) throws MessagingException {
503         return headers.getNonMatchingHeaders(name);
504     }
505 
506     public void addHeaderLine(String line) throws MessagingException {
507         headers.addHeaderLine(line);
508     }
509 
510     public Enumeration getAllHeaderLines() throws MessagingException {
511         return headers.getAllHeaderLines();
512     }
513 
514     public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
515         return headers.getMatchingHeaderLines(names);
516     }
517 
518     public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
519         return headers.getNonMatchingHeaderLines(names);
520     }
521 
522     protected void updateHeaders() throws MessagingException {
523         DataHandler handler = getDataHandler();
524 
525         try {
526             // figure out the content type.  If not set, we'll need to figure this out.
527             String type = dh.getContentType();
528             // parse this content type out so we can do matches/compares.
529             ContentType content = new ContentType(type);
530             // is this a multipart content?
531             if (content.match("multipart/*")) {
532                 // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
533                 try {
534                     MimeMultipart part = (MimeMultipart)handler.getContent();
535                     part.updateHeaders();
536                 } catch (ClassCastException e) {
537                     throw new MessagingException("Message content is not MimeMultipart", e);
538                 }
539             }
540             else if (!content.match("message/rfc822")) {
541                 // simple part, we need to update the header type information
542                 // if no encoding is set yet, figure this out from the data handler.
543                 if (getSingleHeader("Content-Transfer-Encoding") == null) {
544                     setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
545                 }
546 
547                 // is a content type header set?  Check the property to see if we need to set this.
548                 if (getHeader("Content-Type") == null) {
549                     if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) {
550                         // is this a text type?  Figure out the encoding and make sure it is set.
551                         if (content.match("text/*")) {
552                             // the charset should be specified as a parameter on the MIME type.  If not there,
553                             // try to figure one out.
554                             if (content.getParameter("charset") == null) {
555 
556                                 String encoding = getEncoding();
557                                 // if we're sending this as 7-bit ASCII, our character set need to be
558                                 // compatible.
559                                 if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
560                                     content.setParameter("charset", "us-ascii");
561                                 }
562                                 else {
563                                     // get the global default.
564                                     content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
565                                 }
566                             }
567                         }
568                     }
569                 }
570             }
571 
572             // if we don't have a content type header, then create one.
573             if (getSingleHeader("Content-Type") == null) {
574                 // get the disposition header, and if it is there, copy the filename parameter into the
575                 // name parameter of the type.
576                 String disp = getHeader("Content-Disposition", null);
577                 if (disp != null) {
578                     // parse up the string value of the disposition
579                     ContentDisposition disposition = new ContentDisposition(disp);
580                     // now check for a filename value
581                     String filename = disposition.getParameter("filename");
582                     // copy and rename the parameter, if it exists.
583                     if (filename != null) {
584                         content.setParameter("name", filename);
585                     }
586                 }
587                 // set the header with the updated content type information.
588                 setHeader("Content-Type", content.toString());
589             }
590 
591         } catch (IOException e) {
592             throw new MessagingException("Error updating message headers", e);
593         }
594     }
595 
596     private String getSingleHeader(String name) throws MessagingException {
597         String[] values = getHeader(name);
598         if (values == null || values.length == 0) {
599             return null;
600         } else {
601             return values[0];
602         }
603     }
604 
605 
606     /**
607      * Attach a file to this body part from a File object.
608      *
609      * @param file   The source File object.
610      *
611      * @exception IOException
612      * @exception MessagingException
613      */
614     public void attachFile(File file) throws IOException, MessagingException {
615     	FileDataSource dataSource = new FileDataSource(file);
616         setDataHandler(new DataHandler(dataSource));
617         setFileName(dataSource.getName());
618     }
619 
620 
621     /**
622      * Attach a file to this body part from a file name.
623      *
624      * @param file   The source file name.
625      *
626      * @exception IOException
627      * @exception MessagingException
628      */
629     public void attachFile(String file) throws IOException, MessagingException {
630         // just create a File object and attach.
631         attachFile(new File(file));
632     }
633 
634 
635     /**
636      * Save the body part content to a given target file.
637      *
638      * @param file   The File object used to store the information.
639      *
640      * @exception IOException
641      * @exception MessagingException
642      */
643     public void saveFile(File file) throws IOException, MessagingException {
644     	OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
645         // we need to read the data in to write it out (sigh).
646         InputStream in = getInputStream();
647         try {
648             byte[] buffer = new byte[8192];
649 	        int length;
650 	        while ((length = in.read(buffer)) > 0) {
651          		out.write(buffer, 0, length);
652             }
653         }
654         finally {
655             // make sure all of the streams are closed before we return
656             if (in != null) {
657                 in.close();
658             }
659             if (out != null) {
660                 out.close();
661             }
662         }
663     }
664 
665 
666     /**
667      * Save the body part content to a given target file.
668      *
669      * @param file   The file name used to store the information.
670      *
671      * @exception IOException
672      * @exception MessagingException
673      */
674     public void saveFile(String file) throws IOException, MessagingException {
675         saveFile(new File(file));
676     }
677 }