View Javadoc

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