001    /**
002     *
003     * Copyright 2003-2006 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.BufferedOutputStream;
021    import java.io.ByteArrayInputStream;
022    import java.io.ByteArrayOutputStream;
023    import java.io.FileOutputStream;
024    import java.io.File;
025    import java.io.IOException;
026    import java.io.InputStream;
027    import java.io.OutputStream;
028    import java.io.UnsupportedEncodingException;
029    import java.util.Enumeration;
030    
031    import javax.activation.DataHandler;
032    import javax.activation.FileDataSource;
033    import javax.mail.BodyPart;
034    import javax.mail.MessagingException;
035    import javax.mail.Multipart;
036    import javax.mail.Part;
037    import javax.mail.internet.HeaderTokenizer.Token;
038    import javax.swing.text.AbstractDocument.Content;
039    
040    import org.apache.geronimo.mail.util.ASCIIUtil;
041    import org.apache.geronimo.mail.util.SessionUtil;
042    
043    /**
044     * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $
045     */
046    public class MimeBodyPart extends BodyPart implements MimePart {
047             // constants for accessed properties
048        private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
049        private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
050        private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset";
051        private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename";
052    
053    
054        /**
055         * The {@link DataHandler} for this Message's content.
056         */
057        protected DataHandler dh;
058        /**
059         * This message's content (unless sourced from a SharedInputStream).
060         */
061        protected byte content[];
062        /**
063         * If the data for this message was supplied by a {@link SharedInputStream}
064         * then this is another such stream representing the content of this message;
065         * if this field is non-null, then {@link #content} will be null.
066         */
067        protected InputStream contentStream;
068        /**
069         * This message's headers.
070         */
071        protected InternetHeaders headers;
072    
073        public MimeBodyPart() {
074            headers = new InternetHeaders();
075        }
076    
077        public MimeBodyPart(InputStream in) throws MessagingException {
078            headers = new InternetHeaders(in);
079            ByteArrayOutputStream baos = new ByteArrayOutputStream();
080            byte[] buffer = new byte[1024];
081            int count;
082            try {
083                while((count = in.read(buffer, 0, 1024)) != -1)
084                    baos.write(buffer, 0, count);
085            } catch (IOException e) {
086                throw new MessagingException(e.toString(),e);
087            }
088            content = baos.toByteArray();
089        }
090    
091        public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
092            this.headers = headers;
093            this.content = content;
094        }
095    
096        /**
097         * Return the content size of this message.  This is obtained
098         * either from the size of the content field (if available) or
099         * 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    }