View Javadoc

1   /**
2    *
3    * Copyright 2003-2006 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package javax.mail.internet;
19  
20  import java.io.BufferedOutputStream;
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.FileOutputStream;
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.io.UnsupportedEncodingException;
29  import java.util.Enumeration;
30  
31  import javax.activation.DataHandler;
32  import javax.activation.FileDataSource;
33  import javax.mail.BodyPart;
34  import javax.mail.MessagingException;
35  import javax.mail.Multipart;
36  import javax.mail.Part;
37  import javax.mail.internet.HeaderTokenizer.Token;
38  import javax.swing.text.AbstractDocument.Content;
39  
40  import org.apache.geronimo.mail.util.ASCIIUtil;
41  import org.apache.geronimo.mail.util.SessionUtil;
42  
43  /**
44   * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $
45   */
46  public class MimeBodyPart extends BodyPart implements MimePart {
47  	 // constants for accessed properties
48      private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
49      private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
50      private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset";
51      private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename";
52  
53  
54      /**
55       * The {@link DataHandler} for this Message's content.
56       */
57      protected DataHandler dh;
58      /**
59       * This message's content (unless sourced from a SharedInputStream).
60       */
61      protected byte content[];
62      /**
63       * If the data for this message was supplied by a {@link SharedInputStream}
64       * then this is another such stream representing the content of this message;
65       * if this field is non-null, then {@link #content} will be null.
66       */
67      protected InputStream contentStream;
68      /**
69       * This message's headers.
70       */
71      protected InternetHeaders headers;
72  
73      public MimeBodyPart() {
74          headers = new InternetHeaders();
75      }
76  
77      public MimeBodyPart(InputStream in) throws MessagingException {
78          headers = new InternetHeaders(in);
79          ByteArrayOutputStream baos = new ByteArrayOutputStream();
80          byte[] buffer = new byte[1024];
81          int count;
82          try {
83              while((count = in.read(buffer, 0, 1024)) != -1)
84                  baos.write(buffer, 0, count);
85          } catch (IOException e) {
86              throw new MessagingException(e.toString(),e);
87          }
88          content = baos.toByteArray();
89      }
90  
91      public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
92          this.headers = headers;
93          this.content = content;
94      }
95  
96      /**
97       * Return the content size of this message.  This is obtained
98       * either from the size of the content field (if available) or
99       * from the contentStream, IFF the contentStream returns a positive
100      * size.  Returns -1 if the size is not available.
101      *
102      * @return Size of the content in bytes.
103      * @exception MessagingException
104      */
105     public int getSize() throws MessagingException {
106         if (content != null) {
107             return content.length;
108         }
109         if (contentStream != null) {
110             try {
111                 int size = contentStream.available();
112                 if (size > 0) {
113                     return size;
114                 }
115             } catch (IOException e) {
116             }
117         }
118         return -1;
119     }
120 
121     public int getLineCount() throws MessagingException {
122         return -1;
123     }
124 
125     public String getContentType() throws MessagingException {
126         String value = getSingleHeader("Content-Type");
127         if (value == null) {
128             value = "text/plain";
129         }
130         return value;
131     }
132 
133     /**
134      * Tests to see if this message has a mime-type match with the
135      * given type name.
136      *
137      * @param type   The tested type name.
138      *
139      * @return If this is a type match on the primary and secondare portion of the types.
140      * @exception MessagingException
141      */
142     public boolean isMimeType(String type) throws MessagingException {
143         return new ContentType(getContentType()).match(type);
144     }
145 
146     /**
147      * Retrieve the message "Content-Disposition" header field.
148      * This value represents how the part should be represented to
149      * the user.
150      *
151      * @return The string value of the Content-Disposition field.
152      * @exception MessagingException
153      */
154     public String getDisposition() throws MessagingException {
155         String disp = getSingleHeader("Content-Disposition");
156         if (disp != null) {
157             return new ContentDisposition(disp).getDisposition();
158         }
159         return null;
160     }
161 
162     /**
163      * Set a new dispostion value for the "Content-Disposition" field.
164      * If the new value is null, the header is removed.
165      *
166      * @param disposition
167      *               The new disposition value.
168      *
169      * @exception MessagingException
170      */
171     public void setDisposition(String disposition) throws MessagingException {
172         if (disposition == null) {
173             removeHeader("Content-Disposition");
174         }
175         else {
176             // the disposition has parameters, which we'll attempt to preserve in any existing header.
177             String currentHeader = getSingleHeader("Content-Disposition");
178             if (currentHeader != null) {
179                 ContentDisposition content = new ContentDisposition(currentHeader);
180                 content.setDisposition(disposition);
181                 setHeader("Content-Disposition", content.toString());
182             }
183             else {
184                 // set using the raw string.
185                 setHeader("Content-Disposition", disposition);
186             }
187         }
188     }
189 
190     /**
191      * Retrieves the current value of the "Content-Transfer-Encoding"
192      * header.  Returns null if the header does not exist.
193      *
194      * @return The current header value or null.
195      * @exception MessagingException
196      */
197     public String getEncoding() throws MessagingException {
198         // this might require some parsing to sort out.
199         String encoding = getSingleHeader("Content-Transfer-Encoding");
200         if (encoding != null) {
201             // we need to parse this into ATOMs and other constituent parts.  We want the first
202             // ATOM token on the string.
203             HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
204 
205             Token token = tokenizer.next();
206             while (token.getType() != Token.EOF) {
207                 // if this is an ATOM type, return it.
208                 if (token.getType() == Token.ATOM) {
209                     return token.getValue();
210                 }
211             }
212             // not ATOMs found, just return the entire header value....somebody might be able to make sense of
213             // this.
214             return encoding;
215         }
216         // no header, nothing to return.
217         return null;
218     }
219 
220 
221     /**
222      * Retrieve the value of the "Content-ID" header.  Returns null
223      * if the header does not exist.
224      *
225      * @return The current header value or null.
226      * @exception MessagingException
227      */
228     public String getContentID() throws MessagingException {
229         return getSingleHeader("Content-ID");
230     }
231 
232     public void setContentID(String cid) throws MessagingException {
233         setOrRemoveHeader("Content-ID", cid);
234     }
235 
236     public String getContentMD5() throws MessagingException {
237         return getSingleHeader("Content-MD5");
238     }
239 
240     public void setContentMD5(String md5) throws MessagingException {
241         setHeader("Content-MD5", md5);
242     }
243 
244     public String[] getContentLanguage() throws MessagingException {
245         return getHeader("Content-Language");
246     }
247 
248     public void setContentLanguage(String[] languages) throws MessagingException {
249         if (languages == null) {
250             removeHeader("Content-Language");
251         } else if (languages.length == 1) {
252             setHeader("Content-Language", languages[0]);
253         } else {
254             StringBuffer buf = new StringBuffer(languages.length * 20);
255             buf.append(languages[0]);
256             for (int i = 1; i < languages.length; i++) {
257                 buf.append(',').append(languages[i]);
258             }
259             setHeader("Content-Language", buf.toString());
260         }
261     }
262 
263     public String getDescription() throws MessagingException {
264         String description = getSingleHeader("Content-Description");
265         if (description != null) {
266             try {
267                 // this could be both folded and encoded.  Return this to usable form.
268                 return MimeUtility.decodeText(MimeUtility.unfold(description));
269             } catch (UnsupportedEncodingException e) {
270                 // ignore
271             }
272         }
273         // return the raw version for any errors.
274         return description;
275     }
276 
277     public void setDescription(String description) throws MessagingException {
278         setDescription(description, null);
279     }
280 
281     public void setDescription(String description, String charset) throws MessagingException {
282         if (description == null) {
283             removeHeader("Content-Description");
284         }
285         else {
286             try {
287                 setHeader("Content-Description", MimeUtility.fold(21, MimeUtility.encodeText(description, charset, null)));
288             } catch (UnsupportedEncodingException e) {
289                 throw new MessagingException(e.getMessage(), e);
290             }
291         }
292     }
293 
294     public String getFileName() throws MessagingException {
295         // see if there is a disposition.  If there is, parse off the filename parameter.
296         String disposition = getSingleHeader("Content-Disposition");
297         String filename = null;
298 
299         if (disposition != null) {
300             filename = new ContentDisposition(disposition).getParameter("filename");
301         }
302 
303         // if there's no filename on the disposition, there might be a name parameter on a
304         // Content-Type header.
305         if (filename == null) {
306             String type = getSingleHeader("Content-Type");
307             if (type != null) {
308                 try {
309                     filename = new ContentType(type).getParameter("name");
310                 } catch (ParseException e) {
311                 }
312             }
313         }
314         // if we have a name, we might need to decode this if an additional property is set.
315         if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) {
316             try {
317                 filename = MimeUtility.decodeText(filename);
318             } catch (UnsupportedEncodingException e) {
319                 throw new MessagingException("Unable to decode filename", e);
320             }
321         }
322 
323         return filename;
324     }
325 
326 
327     public void setFileName(String name) throws MessagingException {
328         System.out.println("Setting file name to " + name);
329         // there's an optional session property that requests file name encoding...we need to process this before
330         // setting the value.
331         if (name != null && SessionUtil.getBooleanProperty(MIME_ENCODEFILENAME, false)) {
332             try {
333                 name = MimeUtility.encodeText(name);
334             } catch (UnsupportedEncodingException e) {
335                 throw new MessagingException("Unable to encode filename", e);
336             }
337         }
338 
339         // get the disposition string.
340         String disposition = getDisposition();
341         // if not there, then this is an attachment.
342         if (disposition == null) {
343             disposition = Part.ATTACHMENT;
344         }
345 
346         // now create a disposition object and set the parameter.
347         ContentDisposition contentDisposition = new ContentDisposition(disposition);
348         contentDisposition.setParameter("filename", name);
349 
350         // serialize this back out and reset.
351         setHeader("Content-Disposition", contentDisposition.toString());
352 
353         // The Sun implementation appears to update the Content-type name parameter too, based on
354         // another system property
355         if (SessionUtil.getBooleanProperty(MIME_SETCONTENTTYPEFILENAME, true)) {
356             ContentType type = new ContentType(getContentType());
357             type.setParameter("name", name);
358             setHeader("Content-Type", type.toString());
359         }
360     }
361 
362     public InputStream getInputStream() throws MessagingException, IOException {
363         return getDataHandler().getInputStream();
364     }
365 
366     protected InputStream getContentStream() throws MessagingException {
367         if (contentStream != null) {
368             return contentStream;
369         }
370 
371         if (content != null) {
372             return new ByteArrayInputStream(content);
373         } else {
374             throw new MessagingException("No content");
375         }
376     }
377 
378     public InputStream getRawInputStream() throws MessagingException {
379         return getContentStream();
380     }
381 
382     public synchronized DataHandler getDataHandler() throws MessagingException {
383         if (dh == null) {
384             dh = new DataHandler(new MimePartDataSource(this));
385         }
386         return dh;
387     }
388 
389     public Object getContent() throws MessagingException, IOException {
390         return getDataHandler().getContent();
391     }
392 
393     public void setDataHandler(DataHandler handler) throws MessagingException {
394         dh = handler;
395         // if we have a handler override, then we need to invalidate any content
396         // headers that define the types.  This information will be derived from the
397         // data heander unless subsequently overridden.
398         removeHeader("Content-Type");
399         removeHeader("Content-Transfer-Encoding");
400 
401     }
402 
403     public void setContent(Object content, String type) throws MessagingException {
404         // Multipart content needs to be handled separately.
405         if (content instanceof Multipart) {
406             setContent((Multipart)content);
407         }
408         else {
409             setDataHandler(new DataHandler(content, type));
410         }
411 
412     }
413 
414     public void setText(String text) throws MessagingException {
415         setText(text, null);
416     }
417 
418     public void setText(String text, String charset) throws MessagingException {
419         // the default subtype is plain text.
420         setText(text, charset, "plain");
421     }
422 
423 
424     public void setText(String text, String charset, String subtype) throws MessagingException {
425         // we need to sort out the character set if one is not provided.
426         if (charset == null) {
427             // if we have non us-ascii characters here, we need to adjust this.
428             if (!ASCIIUtil.isAscii(text)) {
429                 charset = MimeUtility.getDefaultMIMECharset();
430             }
431             else {
432                 charset = "us-ascii";
433             }
434         }
435         setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME));
436     }
437 
438     public void setContent(Multipart part) throws MessagingException {
439         setDataHandler(new DataHandler(part, part.getContentType()));
440         part.setParent(this);
441     }
442 
443     public void writeTo(OutputStream out) throws IOException, MessagingException {
444         headers.writeTo(out, null);
445         // add the separater between the headers and the data portion.
446         out.write('\r');
447         out.write('\n');
448         // we need to process this using the transfer encoding type
449         OutputStream encodingStream = MimeUtility.encode(out, getEncoding());
450         getDataHandler().writeTo(encodingStream);
451         encodingStream.flush();
452     }
453 
454     public String[] getHeader(String name) throws MessagingException {
455         return headers.getHeader(name);
456     }
457 
458     public String getHeader(String name, String delimiter) throws MessagingException {
459         return headers.getHeader(name, delimiter);
460     }
461 
462     public void setHeader(String name, String value) throws MessagingException {
463         headers.setHeader(name, value);
464     }
465 
466     /**
467      * Conditionally set or remove a named header.  If the new value
468      * is null, the header is removed.
469      *
470      * @param name   The header name.
471      * @param value  The new header value.  A null value causes the header to be
472      *               removed.
473      *
474      * @exception MessagingException
475      */
476     private void setOrRemoveHeader(String name, String value) throws MessagingException {
477         if (value == null) {
478             headers.removeHeader(name);
479         }
480         else {
481             headers.setHeader(name, value);
482         }
483     }
484 
485     public void addHeader(String name, String value) throws MessagingException {
486         headers.addHeader(name, value);
487     }
488 
489     public void removeHeader(String name) throws MessagingException {
490         headers.removeHeader(name);
491     }
492 
493     public Enumeration getAllHeaders() throws MessagingException {
494         return headers.getAllHeaders();
495     }
496 
497     public Enumeration getMatchingHeaders(String[] name) throws MessagingException {
498         return headers.getMatchingHeaders(name);
499     }
500 
501     public Enumeration getNonMatchingHeaders(String[] name) throws MessagingException {
502         return headers.getNonMatchingHeaders(name);
503     }
504 
505     public void addHeaderLine(String line) throws MessagingException {
506         headers.addHeaderLine(line);
507     }
508 
509     public Enumeration getAllHeaderLines() throws MessagingException {
510         return headers.getAllHeaderLines();
511     }
512 
513     public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
514         return headers.getMatchingHeaderLines(names);
515     }
516 
517     public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
518         return headers.getNonMatchingHeaderLines(names);
519     }
520 
521     protected void updateHeaders() throws MessagingException {
522         DataHandler handler = getDataHandler();
523 
524         try {
525             // figure out the content type.  If not set, we'll need to figure this out.
526             String type = dh.getContentType();
527             // parse this content type out so we can do matches/compares.
528             ContentType content = new ContentType(type);
529             // is this a multipart content?
530             if (content.match("multipart/*")) {
531                 // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
532                 try {
533                     MimeMultipart part = (MimeMultipart)handler.getContent();
534                     part.updateHeaders();
535                 } catch (ClassCastException e) {
536                     throw new MessagingException("Message content is not MimeMultipart", e);
537                 }
538             }
539             else if (!content.match("message/rfc822")) {
540                 // simple part, we need to update the header type information
541                 // if no encoding is set yet, figure this out from the data handler.
542                 if (getSingleHeader("Content-Transfer-Encoding") == null) {
543                     setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
544                 }
545 
546                 // is a content type header set?  Check the property to see if we need to set this.
547                 if (getHeader("Content-Type") == null) {
548                     if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) {
549                         // is this a text type?  Figure out the encoding and make sure it is set.
550                         if (content.match("text/*")) {
551                             // the charset should be specified as a parameter on the MIME type.  If not there,
552                             // try to figure one out.
553                             if (content.getParameter("charset") == null) {
554 
555                                 String encoding = getEncoding();
556                                 // if we're sending this as 7-bit ASCII, our character set need to be
557                                 // compatible.
558                                 if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
559                                     content.setParameter("charset", "us-ascii");
560                                 }
561                                 else {
562                                     // get the global default.
563                                     content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
564                                 }
565                             }
566                         }
567                     }
568                 }
569             }
570 
571             // if we don't have a content type header, then create one.
572             if (getSingleHeader("Content-Type") == null) {
573                 // get the disposition header, and if it is there, copy the filename parameter into the
574                 // name parameter of the type.
575                 String disp = getHeader("Content-Disposition", null);
576                 if (disp != null) {
577                     // parse up the string value of the disposition
578                     ContentDisposition disposition = new ContentDisposition(disp);
579                     // now check for a filename value
580                     String filename = disposition.getParameter("filename");
581                     // copy and rename the parameter, if it exists.
582                     if (filename != null) {
583                         content.setParameter("name", filename);
584                     }
585                 }
586                 // set the header with the updated content type information.
587                 setHeader("Content-Type", content.toString());
588             }
589 
590         } catch (IOException e) {
591             throw new MessagingException("Error updating message headers", e);
592         }
593     }
594 
595     private String getSingleHeader(String name) throws MessagingException {
596         String[] values = getHeader(name);
597         if (values == null || values.length == 0) {
598             return null;
599         } else {
600             return values[0];
601         }
602     }
603 
604 
605     /**
606      * Attach a file to this body part from a File object.
607      *
608      * @param file   The source File object.
609      *
610      * @exception IOException
611      * @exception MessagingException
612      */
613     public void attachFile(File file) throws IOException, MessagingException {
614     	FileDataSource dataSource = new FileDataSource(file);
615         setDataHandler(new DataHandler(dataSource));
616         setFileName(dataSource.getName());
617     }
618 
619 
620     /**
621      * Attach a file to this body part from a file name.
622      *
623      * @param file   The source file name.
624      *
625      * @exception IOException
626      * @exception MessagingException
627      */
628     public void attachFile(String file) throws IOException, MessagingException {
629         // just create a File object and attach.
630         attachFile(new File(file));
631     }
632 
633 
634     /**
635      * Save the body part content to a given target file.
636      *
637      * @param file   The File object used to store the information.
638      *
639      * @exception IOException
640      * @exception MessagingException
641      */
642     public void saveFile(File file) throws IOException, MessagingException {
643     	OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
644         // we need to read the data in to write it out (sigh).
645         InputStream in = getInputStream();
646         try {
647             byte[] buffer = new byte[8192];
648 	        int length;
649 	        while ((length = in.read(buffer)) > 0) {
650          		out.write(buffer, 0, length);
651             }
652         }
653         finally {
654             // make sure all of the streams are closed before we return
655             if (in != null) {
656                 in.close();
657             }
658             if (out != null) {
659                 out.close();
660             }
661         }
662     }
663 
664 
665     /**
666      * Save the body part content to a given target file.
667      *
668      * @param file   The file name used to store the information.
669      *
670      * @exception IOException
671      * @exception MessagingException
672      */
673     public void saveFile(String file) throws IOException, MessagingException {
674         saveFile(new File(file));
675     }
676 }