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: 549494 $ $Date: 2007-06-21 10:28:09 -0400 (Thu, 21 Jun 2007) $
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            } catch (IOException e) {
088                throw new MessagingException(e.toString(),e);
089            }
090            content = baos.toByteArray();
091        }
092    
093        public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
094            this.headers = headers;
095            this.content = content;
096        }
097    
098        /**
099         * Return the content size of this message.  This is obtained
100         * either from the size of the content field (if available) or
101         * from the contentStream, IFF the contentStream returns a positive
102         * size.  Returns -1 if the size is not available.
103         *
104         * @return Size of the content in bytes.
105         * @exception MessagingException
106         */
107        public int getSize() throws MessagingException {
108            if (content != null) {
109                return content.length;
110            }
111            if (contentStream != null) {
112                try {
113                    int size = contentStream.available();
114                    if (size > 0) {
115                        return size;
116                    }
117                } catch (IOException e) {
118                }
119            }
120            return -1;
121        }
122    
123        public int getLineCount() throws MessagingException {
124            return -1;
125        }
126    
127        public String getContentType() throws MessagingException {
128            String value = getSingleHeader("Content-Type");
129            if (value == null) {
130                value = "text/plain";
131            }
132            return value;
133        }
134    
135        /**
136         * Tests to see if this message has a mime-type match with the
137         * given type name.
138         *
139         * @param type   The tested type name.
140         *
141         * @return If this is a type match on the primary and secondare portion of the types.
142         * @exception MessagingException
143         */
144        public boolean isMimeType(String type) throws MessagingException {
145            return new ContentType(getContentType()).match(type);
146        }
147    
148        /**
149         * Retrieve the message "Content-Disposition" header field.
150         * This value represents how the part should be represented to
151         * the user.
152         *
153         * @return The string value of the Content-Disposition field.
154         * @exception MessagingException
155         */
156        public String getDisposition() throws MessagingException {
157            String disp = getSingleHeader("Content-Disposition");
158            if (disp != null) {
159                return new ContentDisposition(disp).getDisposition();
160            }
161            return null;
162        }
163    
164        /**
165         * Set a new dispostion value for the "Content-Disposition" field.
166         * If the new value is null, the header is removed.
167         *
168         * @param disposition
169         *               The new disposition value.
170         *
171         * @exception MessagingException
172         */
173        public void setDisposition(String disposition) throws MessagingException {
174            if (disposition == null) {
175                removeHeader("Content-Disposition");
176            }
177            else {
178                // the disposition has parameters, which we'll attempt to preserve in any existing header.
179                String currentHeader = getSingleHeader("Content-Disposition");
180                if (currentHeader != null) {
181                    ContentDisposition content = new ContentDisposition(currentHeader);
182                    content.setDisposition(disposition);
183                    setHeader("Content-Disposition", content.toString());
184                }
185                else {
186                    // set using the raw string.
187                    setHeader("Content-Disposition", disposition);
188                }
189            }
190        }
191    
192        /**
193         * Retrieves the current value of the "Content-Transfer-Encoding"
194         * header.  Returns null if the header does not exist.
195         *
196         * @return The current header value or null.
197         * @exception MessagingException
198         */
199        public String getEncoding() throws MessagingException {
200            // this might require some parsing to sort out.
201            String encoding = getSingleHeader("Content-Transfer-Encoding");
202            if (encoding != null) {
203                // we need to parse this into ATOMs and other constituent parts.  We want the first
204                // ATOM token on the string.
205                HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
206    
207                Token token = tokenizer.next();
208                while (token.getType() != Token.EOF) {
209                    // if this is an ATOM type, return it.
210                    if (token.getType() == Token.ATOM) {
211                        return token.getValue();
212                    }
213                }
214                // not ATOMs found, just return the entire header value....somebody might be able to make sense of
215                // this.
216                return encoding;
217            }
218            // no header, nothing to return.
219            return null;
220        }
221    
222    
223        /**
224         * Retrieve the value of the "Content-ID" header.  Returns null
225         * if the header does not exist.
226         *
227         * @return The current header value or null.
228         * @exception MessagingException
229         */
230        public String getContentID() throws MessagingException {
231            return getSingleHeader("Content-ID");
232        }
233    
234        public void setContentID(String cid) throws MessagingException {
235            setOrRemoveHeader("Content-ID", cid);
236        }
237    
238        public String getContentMD5() throws MessagingException {
239            return getSingleHeader("Content-MD5");
240        }
241    
242        public void setContentMD5(String md5) throws MessagingException {
243            setHeader("Content-MD5", md5);
244        }
245    
246        public String[] getContentLanguage() throws MessagingException {
247            return getHeader("Content-Language");
248        }
249    
250        public void setContentLanguage(String[] languages) throws MessagingException {
251            if (languages == null) {
252                removeHeader("Content-Language");
253            } else if (languages.length == 1) {
254                setHeader("Content-Language", languages[0]);
255            } else {
256                StringBuffer buf = new StringBuffer(languages.length * 20);
257                buf.append(languages[0]);
258                for (int i = 1; i < languages.length; i++) {
259                    buf.append(',').append(languages[i]);
260                }
261                setHeader("Content-Language", buf.toString());
262            }
263        }
264    
265        public String getDescription() throws MessagingException {
266            String description = getSingleHeader("Content-Description");
267            if (description != null) {
268                try {
269                    // this could be both folded and encoded.  Return this to usable form.
270                    return MimeUtility.decodeText(MimeUtility.unfold(description));
271                } catch (UnsupportedEncodingException e) {
272                    // ignore
273                }
274            }
275            // return the raw version for any errors.
276            return description;
277        }
278    
279        public void setDescription(String description) throws MessagingException {
280            setDescription(description, null);
281        }
282    
283        public void setDescription(String description, String charset) throws MessagingException {
284            if (description == null) {
285                removeHeader("Content-Description");
286            }
287            else {
288                try {
289                    setHeader("Content-Description", MimeUtility.fold(21, MimeUtility.encodeText(description, charset, null)));
290                } catch (UnsupportedEncodingException e) {
291                    throw new MessagingException(e.getMessage(), e);
292                }
293            }
294        }
295    
296        public String getFileName() throws MessagingException {
297            // see if there is a disposition.  If there is, parse off the filename parameter.
298            String disposition = getSingleHeader("Content-Disposition");
299            String filename = null;
300    
301            if (disposition != null) {
302                filename = new ContentDisposition(disposition).getParameter("filename");
303            }
304    
305            // if there's no filename on the disposition, there might be a name parameter on a
306            // Content-Type header.
307            if (filename == null) {
308                String type = getSingleHeader("Content-Type");
309                if (type != null) {
310                    try {
311                        filename = new ContentType(type).getParameter("name");
312                    } catch (ParseException e) {
313                    }
314                }
315            }
316            // if we have a name, we might need to decode this if an additional property is set.
317            if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) {
318                try {
319                    filename = MimeUtility.decodeText(filename);
320                } catch (UnsupportedEncodingException e) {
321                    throw new MessagingException("Unable to decode filename", e);
322                }
323            }
324    
325            return filename;
326        }
327    
328    
329        public void setFileName(String name) throws MessagingException {
330            System.out.println("Setting file name to " + name);
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                // is this a multipart content?
532                if (content.match("multipart/*")) {
533                    // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
534                    try {
535                        MimeMultipart part = (MimeMultipart)handler.getContent();
536                        part.updateHeaders();
537                    } catch (ClassCastException e) {
538                        throw new MessagingException("Message content is not MimeMultipart", e);
539                    }
540                }
541                else if (!content.match("message/rfc822")) {
542                    // simple part, we need to update the header type information
543                    // if no encoding is set yet, figure this out from the data handler.
544                    if (getSingleHeader("Content-Transfer-Encoding") == null) {
545                        setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
546                    }
547    
548                    // is a content type header set?  Check the property to see if we need to set this.
549                    if (getHeader("Content-Type") == null) {
550                        if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) {
551                            // is this a text type?  Figure out the encoding and make sure it is set.
552                            if (content.match("text/*")) {
553                                // the charset should be specified as a parameter on the MIME type.  If not there,
554                                // try to figure one out.
555                                if (content.getParameter("charset") == null) {
556    
557                                    String encoding = getEncoding();
558                                    // if we're sending this as 7-bit ASCII, our character set need to be
559                                    // compatible.
560                                    if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
561                                        content.setParameter("charset", "us-ascii");
562                                    }
563                                    else {
564                                        // get the global default.
565                                        content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
566                                    }
567                                }
568                            }
569                        }
570                    }
571                }
572    
573                // if we don't have a content type header, then create one.
574                if (getSingleHeader("Content-Type") == null) {
575                    // get the disposition header, and if it is there, copy the filename parameter into the
576                    // name parameter of the type.
577                    String disp = getHeader("Content-Disposition", null);
578                    if (disp != null) {
579                        // parse up the string value of the disposition
580                        ContentDisposition disposition = new ContentDisposition(disp);
581                        // now check for a filename value
582                        String filename = disposition.getParameter("filename");
583                        // copy and rename the parameter, if it exists.
584                        if (filename != null) {
585                            content.setParameter("name", filename);
586                        }
587                    }
588                    // set the header with the updated content type information.
589                    setHeader("Content-Type", content.toString());
590                }
591    
592            } catch (IOException e) {
593                throw new MessagingException("Error updating message headers", e);
594            }
595        }
596    
597        private String getSingleHeader(String name) throws MessagingException {
598            String[] values = getHeader(name);
599            if (values == null || values.length == 0) {
600                return null;
601            } else {
602                return values[0];
603            }
604        }
605    
606    
607        /**
608         * Attach a file to this body part from a File object.
609         *
610         * @param file   The source File object.
611         *
612         * @exception IOException
613         * @exception MessagingException
614         */
615        public void attachFile(File file) throws IOException, MessagingException {
616            FileDataSource dataSource = new FileDataSource(file);
617            setDataHandler(new DataHandler(dataSource));
618            setFileName(dataSource.getName());
619        }
620    
621    
622        /**
623         * Attach a file to this body part from a file name.
624         *
625         * @param file   The source file name.
626         *
627         * @exception IOException
628         * @exception MessagingException
629         */
630        public void attachFile(String file) throws IOException, MessagingException {
631            // just create a File object and attach.
632            attachFile(new File(file));
633        }
634    
635    
636        /**
637         * Save the body part content to a given target file.
638         *
639         * @param file   The File object used to store the information.
640         *
641         * @exception IOException
642         * @exception MessagingException
643         */
644        public void saveFile(File file) throws IOException, MessagingException {
645            OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
646            // we need to read the data in to write it out (sigh).
647            InputStream in = getInputStream();
648            try {
649                byte[] buffer = new byte[8192];
650                    int length;
651                    while ((length = in.read(buffer)) > 0) {
652                            out.write(buffer, 0, length);
653                }
654            }
655            finally {
656                // make sure all of the streams are closed before we return
657                if (in != null) {
658                    in.close();
659                }
660                if (out != null) {
661                    out.close();
662                }
663            }
664        }
665    
666    
667        /**
668         * Save the body part content to a given target file.
669         *
670         * @param file   The file name used to store the information.
671         *
672         * @exception IOException
673         * @exception MessagingException
674         */
675        public void saveFile(String file) throws IOException, MessagingException {
676            saveFile(new File(file));
677        }
678    }