001    /**
002     *
003     * Copyright 2003-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package javax.mail.internet;
019    
020    import java.io.ByteArrayInputStream;
021    import java.io.ByteArrayOutputStream;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.OutputStream;
025    import java.io.UnsupportedEncodingException;
026    import java.util.Enumeration;
027    
028    import javax.activation.DataHandler;
029    import javax.mail.BodyPart;
030    import javax.mail.MessagingException;
031    import javax.mail.Multipart;
032    import javax.mail.Part;
033    import javax.mail.internet.HeaderTokenizer.Token;
034    import javax.swing.text.AbstractDocument.Content;
035    
036    import org.apache.geronimo.mail.util.ASCIIUtil;
037    import org.apache.geronimo.mail.util.SessionUtil;
038    
039    /**
040     * @version $Rev: 412720 $ $Date: 2006-06-08 03:52:48 -0700 (Thu, 08 Jun 2006) $
041     */
042    public class MimeBodyPart extends BodyPart implements MimePart {
043             // constants for accessed properties
044        private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
045        private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
046        private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset";
047        private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename";
048    
049    
050        /**
051         * The {@link DataHandler} for this Message's content.
052         */
053        protected DataHandler dh;
054        /**
055         * This message's content (unless sourced from a SharedInputStream).
056         */
057        protected byte content[];
058        /**
059         * If the data for this message was supplied by a {@link SharedInputStream}
060         * then this is another such stream representing the content of this message;
061         * if this field is non-null, then {@link #content} will be null.
062         */
063        protected InputStream contentStream;
064        /**
065         * This message's headers.
066         */
067        protected InternetHeaders headers;
068    
069        public MimeBodyPart() {
070            headers = new InternetHeaders();
071        }
072    
073        public MimeBodyPart(InputStream in) throws MessagingException {
074            headers = new InternetHeaders(in);
075            ByteArrayOutputStream baos = new ByteArrayOutputStream();
076            byte[] buffer = new byte[1024];
077            int count;
078            try {
079                while((count = in.read(buffer, 0, 1024)) != -1)
080                    baos.write(buffer, 0, count);
081            } catch (IOException e) {
082                throw new MessagingException(e.toString(),e);
083            }
084            content = baos.toByteArray();
085        }
086    
087        public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
088            this.headers = headers;
089            this.content = content;
090        }
091    
092        /**
093         * Return the content size of this message.  This is obtained
094         * either from the size of the content field (if available) or
095         * from the contentStream, IFF the contentStream returns a positive
096         * size.  Returns -1 if the size is not available.
097         *
098         * @return Size of the content in bytes.
099         * @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    }