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