001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package javax.mail.internet;
021    
022    import java.io.ByteArrayInputStream;
023    import java.io.ByteArrayOutputStream;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.OutputStream;
027    import java.io.UnsupportedEncodingException;
028    import java.util.Enumeration;
029    
030    import javax.activation.DataHandler;
031    import javax.mail.BodyPart;
032    import javax.mail.MessagingException;
033    import javax.mail.Multipart;
034    import javax.mail.Part;
035    import javax.mail.internet.HeaderTokenizer.Token;
036    import javax.swing.text.AbstractDocument.Content;
037    
038    import org.apache.geronimo.mail.util.ASCIIUtil;
039    import org.apache.geronimo.mail.util.SessionUtil;
040    
041    /**
042     * @version $Rev: 467553 $ $Date: 2006-10-25 06:01:51 +0200 (Mi, 25. Okt 2006) $
043     */
044    public class MimeBodyPart extends BodyPart implements MimePart {
045             // constants for accessed properties
046        private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
047        private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
048        private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset";
049        private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename";
050    
051    
052        /**
053         * The {@link DataHandler} for this Message's content.
054         */
055        protected DataHandler dh;
056        /**
057         * This message's content (unless sourced from a SharedInputStream).
058         */
059        protected byte content[];
060        /**
061         * If the data for this message was supplied by a {@link SharedInputStream}
062         * then this is another such stream representing the content of this message;
063         * if this field is non-null, then {@link #content} will be null.
064         */
065        protected InputStream contentStream;
066        /**
067         * This message's headers.
068         */
069        protected InternetHeaders headers;
070    
071        public MimeBodyPart() {
072            headers = new InternetHeaders();
073        }
074    
075        public MimeBodyPart(InputStream in) throws MessagingException {
076            headers = new InternetHeaders(in);
077            ByteArrayOutputStream baos = new ByteArrayOutputStream();
078            byte[] buffer = new byte[1024];
079            int count;
080            try {
081                while((count = in.read(buffer, 0, 1024)) != -1)
082                    baos.write(buffer, 0, count);
083            } catch (IOException e) {
084                throw new MessagingException(e.toString(),e);
085            }
086            content = baos.toByteArray();
087        }
088    
089        public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
090            this.headers = headers;
091            this.content = content;
092        }
093    
094        /**
095         * Return the content size of this message.  This is obtained
096         * either from the size of the content field (if available) or
097         * from the contentStream, IFF the contentStream returns a positive
098         * size.  Returns -1 if the size is not available.
099         *
100         * @return Size of the content in bytes.
101         * @exception MessagingException
102         */
103        public int getSize() throws MessagingException {
104            if (content != null) {
105                return content.length;
106            }
107            if (contentStream != null) {
108                try {
109                    int size = contentStream.available();
110                    if (size > 0) {
111                        return size;
112                    }
113                } catch (IOException e) {
114                }
115            }
116            return -1;
117        }
118    
119        public int getLineCount() throws MessagingException {
120            return -1;
121        }
122    
123        public String getContentType() throws MessagingException {
124            String value = getSingleHeader("Content-Type");
125            if (value == null) {
126                value = "text/plain";
127            }
128            return value;
129        }
130    
131        /**
132         * Tests to see if this message has a mime-type match with the
133         * given type name.
134         *
135         * @param type   The tested type name.
136         *
137         * @return If this is a type match on the primary and secondare portion of the types.
138         * @exception MessagingException
139         */
140        public boolean isMimeType(String type) throws MessagingException {
141            return new ContentType(getContentType()).match(type);
142        }
143    
144        /**
145         * Retrieve the message "Content-Disposition" header field.
146         * This value represents how the part should be represented to
147         * the user.
148         *
149         * @return The string value of the Content-Disposition field.
150         * @exception MessagingException
151         */
152        public String getDisposition() throws MessagingException {
153            String disp = getSingleHeader("Content-Disposition");
154            if (disp != null) {
155                return new ContentDisposition(disp).getDisposition();
156            }
157            return null;
158        }
159    
160        /**
161         * Set a new dispostion value for the "Content-Disposition" field.
162         * If the new value is null, the header is removed.
163         *
164         * @param disposition
165         *               The new disposition value.
166         *
167         * @exception MessagingException
168         */
169        public void setDisposition(String disposition) throws MessagingException {
170            if (disposition == null) {
171                removeHeader("Content-Disposition");
172            }
173            else {
174                // the disposition has parameters, which we'll attempt to preserve in any existing header.
175                String currentHeader = getSingleHeader("Content-Disposition");
176                if (currentHeader != null) {
177                    ContentDisposition content = new ContentDisposition(currentHeader);
178                    content.setDisposition(disposition);
179                    setHeader("Content-Disposition", content.toString());
180                }
181                else {
182                    // set using the raw string.
183                    setHeader("Content-Disposition", disposition);
184                }
185            }
186        }
187    
188        /**
189         * Retrieves the current value of the "Content-Transfer-Encoding"
190         * header.  Returns null if the header does not exist.
191         *
192         * @return The current header value or null.
193         * @exception MessagingException
194         */
195        public String getEncoding() throws MessagingException {
196            // this might require some parsing to sort out.
197            String encoding = getSingleHeader("Content-Transfer-Encoding");
198            if (encoding != null) {
199                // we need to parse this into ATOMs and other constituent parts.  We want the first
200                // ATOM token on the string.
201                HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
202    
203                Token token = tokenizer.next();
204                while (token.getType() != Token.EOF) {
205                    // if this is an ATOM type, return it.
206                    if (token.getType() == Token.ATOM) {
207                        return token.getValue();
208                    }
209                }
210                // not ATOMs found, just return the entire header value....somebody might be able to make sense of
211                // this.
212                return encoding;
213            }
214            // no header, nothing to return.
215            return null;
216        }
217    
218    
219        /**
220         * Retrieve the value of the "Content-ID" header.  Returns null
221         * if the header does not exist.
222         *
223         * @return The current header value or null.
224         * @exception MessagingException
225         */
226        public String getContentID() throws MessagingException {
227            return getSingleHeader("Content-ID");
228        }
229    
230        public void setContentID(String cid) throws MessagingException {
231            setOrRemoveHeader("Content-ID", cid);
232        }
233    
234        public String getContentMD5() throws MessagingException {
235            return getSingleHeader("Content-MD5");
236        }
237    
238        public void setContentMD5(String md5) throws MessagingException {
239            setHeader("Content-MD5", md5);
240        }
241    
242        public String[] getContentLanguage() throws MessagingException {
243            return getHeader("Content-Language");
244        }
245    
246        public void setContentLanguage(String[] languages) throws MessagingException {
247            if (languages == null) {
248                removeHeader("Content-Language");
249            } else if (languages.length == 1) {
250                setHeader("Content-Language", languages[0]);
251            } else {
252                StringBuffer buf = new StringBuffer(languages.length * 20);
253                buf.append(languages[0]);
254                for (int i = 1; i < languages.length; i++) {
255                    buf.append(',').append(languages[i]);
256                }
257                setHeader("Content-Language", buf.toString());
258            }
259        }
260    
261        public String getDescription() throws MessagingException {
262            String description = getSingleHeader("Content-Description");
263            if (description != null) {
264                try {
265                    // this could be both folded and encoded.  Return this to usable form.
266                    return MimeUtility.decodeText(ASCIIUtil.unfold(description));
267                } catch (UnsupportedEncodingException e) {
268                    // ignore
269                }
270            }
271            // return the raw version for any errors.
272            return description;
273        }
274    
275        public void setDescription(String description) throws MessagingException {
276            setDescription(description, null);
277        }
278    
279        public void setDescription(String description, String charset) throws MessagingException {
280            if (description == null) {
281                removeHeader("Content-Description");
282            }
283            else {
284                try {
285                    setHeader("Content-Description", ASCIIUtil.fold(21, MimeUtility.encodeText(description, charset, null)));
286                } catch (UnsupportedEncodingException e) {
287                    throw new MessagingException(e.getMessage(), e);
288                }
289            }
290        }
291    
292        public String getFileName() throws MessagingException {
293            // see if there is a disposition.  If there is, parse off the filename parameter.
294            String disposition = getSingleHeader("Content-Disposition");
295            String filename = null;
296    
297            if (disposition != null) {
298                filename = new ContentDisposition(disposition).getParameter("filename");
299            }
300    
301            // if there's no filename on the disposition, there might be a name parameter on a
302            // Content-Type header.
303            if (filename == null) {
304                String type = getSingleHeader("Content-Type");
305                if (type != null) {
306                    try {
307                        filename = new ContentType(type).getParameter("name");
308                    } catch (ParseException e) {
309                    }
310                }
311            }
312            // if we have a name, we might need to decode this if an additional property is set.
313            if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) {
314                try {
315                    filename = MimeUtility.decodeText(filename);
316                } catch (UnsupportedEncodingException e) {
317                    throw new MessagingException("Unable to decode filename", e);
318                }
319            }
320    
321            return filename;
322        }
323    
324    
325        public void setFileName(String name) throws MessagingException {
326            // there's an optional session property that requests file name encoding...we need to process this before
327            // setting the value.
328            if (name != null && SessionUtil.getBooleanProperty(MIME_ENCODEFILENAME, false)) {
329                try {
330                    name = MimeUtility.encodeText(name);
331                } catch (UnsupportedEncodingException e) {
332                    throw new MessagingException("Unable to encode filename", e);
333                }
334            }
335    
336            // get the disposition string.
337            String disposition = getDisposition();
338            // if not there, then this is an attachment.
339            if (disposition == null) {
340                disposition = Part.ATTACHMENT;
341            }
342    
343            // now create a disposition object and set the parameter.
344            ContentDisposition contentDisposition = new ContentDisposition(disposition);
345            contentDisposition.setParameter("filename", name);
346    
347            // serialize this back out and reset.
348            setDisposition(contentDisposition.toString());
349            setHeader("Content-Disposition", contentDisposition.toString());
350    
351            // The Sun implementation appears to update the Content-type name parameter too, based on
352            // another system property
353            if (SessionUtil.getBooleanProperty(MIME_SETCONTENTTYPEFILENAME, true)) {
354                ContentType type = new ContentType(getContentType());
355                type.setParameter("name", name);
356                setHeader("Content-Type", type.toString());
357            }
358        }
359    
360        public InputStream getInputStream() throws MessagingException, IOException {
361            return getDataHandler().getInputStream();
362        }
363    
364        protected InputStream getContentStream() throws MessagingException {
365            if (contentStream != null) {
366                return contentStream;
367            }
368    
369            if (content != null) {
370                return new ByteArrayInputStream(content);
371            } else {
372                throw new MessagingException("No content");
373            }
374        }
375    
376        public InputStream getRawInputStream() throws MessagingException {
377            return getContentStream();
378        }
379    
380        public synchronized DataHandler getDataHandler() throws MessagingException {
381            if (dh == null) {
382                dh = new DataHandler(new MimePartDataSource(this));
383            }
384            return dh;
385        }
386    
387        public Object getContent() throws MessagingException, IOException {
388            return getDataHandler().getContent();
389        }
390    
391        public void setDataHandler(DataHandler handler) throws MessagingException {
392            dh = handler;
393            // if we have a handler override, then we need to invalidate any content
394            // headers that define the types.  This information will be derived from the
395            // data heander unless subsequently overridden.
396            removeHeader("Content-Type");
397            removeHeader("Content-Transfer-Encoding");
398    
399        }
400    
401        public void setContent(Object content, String type) throws MessagingException {
402            // Multipart content needs to be handled separately.
403            if (content instanceof Multipart) {
404                setContent((Multipart)content);
405            }
406            else {
407                setDataHandler(new DataHandler(content, type));
408            }
409        }
410    
411        public void setText(String text) throws MessagingException {
412            setText(text, null);
413        }
414    
415        public void setText(String text, String charset) throws MessagingException {
416            // we need to sort out the character set if one is not provided.
417            if (charset == null) {
418                // if we have non us-ascii characters here, we need to adjust this.
419                if (!ASCIIUtil.isAscii(text)) {
420                    charset = MimeUtility.getDefaultMIMECharset();
421                }
422                else {
423                    charset = "us-ascii";
424                }
425            }
426            setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME));
427        }
428    
429        public void setContent(Multipart part) throws MessagingException {
430            setDataHandler(new DataHandler(part, part.getContentType()));
431            part.setParent(this);
432        }
433    
434        public void writeTo(OutputStream out) throws IOException, MessagingException {
435            headers.writeTo(out, null);
436            // add the separater between the headers and the data portion.
437            out.write('\r');
438            out.write('\n');
439            // we need to process this using the transfer encoding type
440            OutputStream encodingStream = MimeUtility.encode(out, getEncoding());
441            getDataHandler().writeTo(encodingStream);
442            encodingStream.flush();
443        }
444    
445        public String[] getHeader(String name) throws MessagingException {
446            return headers.getHeader(name);
447        }
448    
449        public String getHeader(String name, String delimiter) throws MessagingException {
450            return headers.getHeader(name, delimiter);
451        }
452    
453        public void setHeader(String name, String value) throws MessagingException {
454            headers.setHeader(name, value);
455        }
456    
457        /**
458         * Conditionally set or remove a named header.  If the new value
459         * is null, the header is removed.
460         *
461         * @param name   The header name.
462         * @param value  The new header value.  A null value causes the header to be
463         *               removed.
464         *
465         * @exception MessagingException
466         */
467        private void setOrRemoveHeader(String name, String value) throws MessagingException {
468            if (value == null) {
469                headers.removeHeader(name);
470            }
471            else {
472                headers.setHeader(name, value);
473            }
474        }
475    
476        public void addHeader(String name, String value) throws MessagingException {
477            headers.addHeader(name, value);
478        }
479    
480        public void removeHeader(String name) throws MessagingException {
481            headers.removeHeader(name);
482        }
483    
484        public Enumeration getAllHeaders() throws MessagingException {
485            return headers.getAllHeaders();
486        }
487    
488        public Enumeration getMatchingHeaders(String[] name) throws MessagingException {
489            return headers.getMatchingHeaders(name);
490        }
491    
492        public Enumeration getNonMatchingHeaders(String[] name) throws MessagingException {
493            return headers.getNonMatchingHeaders(name);
494        }
495    
496        public void addHeaderLine(String line) throws MessagingException {
497            headers.addHeaderLine(line);
498        }
499    
500        public Enumeration getAllHeaderLines() throws MessagingException {
501            return headers.getAllHeaderLines();
502        }
503    
504        public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
505            return headers.getMatchingHeaderLines(names);
506        }
507    
508        public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
509            return headers.getNonMatchingHeaderLines(names);
510        }
511    
512        protected void updateHeaders() throws MessagingException {
513            DataHandler handler = getDataHandler();
514    
515            try {
516                // figure out the content type.  If not set, we'll need to figure this out.
517                String type = dh.getContentType();
518                // parse this content type out so we can do matches/compares.
519                ContentType content = new ContentType(type);
520                // is this a multipart content?
521                if (content.match("multipart/*")) {
522                    // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
523                    try {
524                        MimeMultipart part = (MimeMultipart)handler.getContent();
525                        part.updateHeaders();
526                    } catch (ClassCastException e) {
527                        throw new MessagingException("Message content is not MimeMultipart", e);
528                    }
529                }
530                else if (!content.match("message/rfc822")) {
531                    // simple part, we need to update the header type information
532                    // if no encoding is set yet, figure this out from the data handler.
533                    if (getSingleHeader("Content-Transfer-Encoding") == null) {
534                        setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
535                    }
536    
537                    // is a content type header set?  Check the property to see if we need to set this.
538                    if (getHeader("Content-Type") == null) {
539                        if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) {
540                            // is this a text type?  Figure out the encoding and make sure it is set.
541                            if (content.match("text/*")) {
542                                // the charset should be specified as a parameter on the MIME type.  If not there,
543                                // try to figure one out.
544                                if (content.getParameter("charset") == null) {
545    
546                                    String encoding = getEncoding();
547                                    // if we're sending this as 7-bit ASCII, our character set need to be
548                                    // compatible.
549                                    if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
550                                        content.setParameter("charset", "us-ascii");
551                                    }
552                                    else {
553                                        // get the global default.
554                                        content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
555                                    }
556                                }
557                            }
558                        }
559                    }
560                }
561    
562                // if we don't have a content type header, then create one.
563                if (getSingleHeader("Content-Type") == null) {
564                    // get the disposition header, and if it is there, copy the filename parameter into the
565                    // name parameter of the type.
566                    String disp = getHeader("Content-Disposition", null);
567                    if (disp != null) {
568                        // parse up the string value of the disposition
569                        ContentDisposition disposition = new ContentDisposition(disp);
570                        // now check for a filename value
571                        String filename = disposition.getParameter("filename");
572                        // copy and rename the parameter, if it exists.
573                        if (filename != null) {
574                            content.setParameter("name", filename);
575                        }
576                    }
577                    // set the header with the updated content type information.
578                    setHeader("Content-Type", content.toString());
579                }
580    
581            } catch (IOException e) {
582                throw new MessagingException("Error updating message headers", e);
583            }
584        }
585    
586        private String getSingleHeader(String name) throws MessagingException {
587            String[] values = getHeader(name);
588            if (values == null || values.length == 0) {
589                return null;
590            } else {
591                return values[0];
592            }
593        }
594    }