/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package javax.mail.internet; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.Enumeration; import javax.activation.DataHandler; import javax.activation.FileDataSource; import javax.mail.BodyPart; import javax.mail.EncodingAware; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Part; import javax.mail.internet.HeaderTokenizer.Token; import org.apache.geronimo.mail.util.ASCIIUtil; import org.apache.geronimo.mail.util.SessionUtil; /** * @version $Rev$ $Date$ */ public class MimeBodyPart extends BodyPart implements MimePart { // constants for accessed properties private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename"; private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename"; private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset"; private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename"; static final boolean cacheMultipart = SessionUtil.getBooleanProperty("mail.mime.cachemultipart", true); /** * The {@link DataHandler} for this Message's content. */ protected DataHandler dh; /** * This message's content (unless sourced from a SharedInputStream). */ /** * If our content is a Multipart or Message object, we save it * the first time it's created by parsing a stream so that changes * to the contained objects will not be lost. * * If this field is not null, it's return by the {@link #getContent} * method. The {@link #getContent} method sets this field if it * would return a Multipart or MimeMessage object. This field is * is cleared by the {@link #setDataHandler} method. * * @since JavaMail 1.5 */ protected Object cachedContent; protected byte content[]; /** * If the data for this message was supplied by a {@link SharedInputStream} * then this is another such stream representing the content of this message; * if this field is non-null, then {@link #content} will be null. */ protected InputStream contentStream; /** * This message's headers. */ protected InternetHeaders headers; public MimeBodyPart() { headers = new InternetHeaders(); } public MimeBodyPart(final InputStream in) throws MessagingException { headers = new InternetHeaders(in); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final byte[] buffer = new byte[1024]; int count; try { while((count = in.read(buffer, 0, 1024)) > 0) { baos.write(buffer, 0, count); } } catch (final IOException e) { throw new MessagingException(e.toString(),e); } content = baos.toByteArray(); } public MimeBodyPart(final InternetHeaders headers, final byte[] content) throws MessagingException { this.headers = headers; this.content = content; } /** * Return the content size of this message. This is obtained * either from the size of the content field (if available) or * from the contentStream, IFF the contentStream returns a positive * size. Returns -1 if the size is not available. * * @return Size of the content in bytes. * @exception MessagingException */ public int getSize() throws MessagingException { if (content != null) { return content.length; } if (contentStream != null) { try { final int size = contentStream.available(); if (size > 0) { return size; } } catch (final IOException e) { } } return -1; } public int getLineCount() throws MessagingException { return -1; } public String getContentType() throws MessagingException { String value = getSingleHeader("Content-Type"); if (value == null) { value = "text/plain"; } return value; } /** * Tests to see if this message has a mime-type match with the * given type name. * * @param type The tested type name. * * @return If this is a type match on the primary and secondare portion of the types. * @exception MessagingException */ public boolean isMimeType(final String type) throws MessagingException { return new ContentType(getContentType()).match(type); } /** * Retrieve the message "Content-Disposition" header field. * This value represents how the part should be represented to * the user. * * @return The string value of the Content-Disposition field. * @exception MessagingException */ public String getDisposition() throws MessagingException { final String disp = getSingleHeader("Content-Disposition"); if (disp != null) { return new ContentDisposition(disp).getDisposition(); } return null; } /** * Set a new dispostion value for the "Content-Disposition" field. * If the new value is null, the header is removed. * * @param disposition * The new disposition value. * * @exception MessagingException */ public void setDisposition(final String disposition) throws MessagingException { if (disposition == null) { removeHeader("Content-Disposition"); } else { // the disposition has parameters, which we'll attempt to preserve in any existing header. final String currentHeader = getSingleHeader("Content-Disposition"); if (currentHeader != null) { final ContentDisposition content = new ContentDisposition(currentHeader); content.setDisposition(disposition); setHeader("Content-Disposition", content.toString()); } else { // set using the raw string. setHeader("Content-Disposition", disposition); } } } /** * Retrieves the current value of the "Content-Transfer-Encoding" * header. Returns null if the header does not exist. * * @return The current header value or null. * @exception MessagingException */ public String getEncoding() throws MessagingException { // this might require some parsing to sort out. final String encoding = getSingleHeader("Content-Transfer-Encoding"); if (encoding != null) { // we need to parse this into ATOMs and other constituent parts. We want the first // ATOM token on the string. final HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME); final Token token = tokenizer.next(); while (token.getType() != Token.EOF) { // if this is an ATOM type, return it. if (token.getType() == Token.ATOM) { return token.getValue(); } } // not ATOMs found, just return the entire header value....somebody might be able to make sense of // this. return encoding; } // no header, nothing to return. return null; } /** * Retrieve the value of the "Content-ID" header. Returns null * if the header does not exist. * * @return The current header value or null. * @exception MessagingException */ public String getContentID() throws MessagingException { return getSingleHeader("Content-ID"); } public void setContentID(final String cid) throws MessagingException { setOrRemoveHeader("Content-ID", cid); } public String getContentMD5() throws MessagingException { return getSingleHeader("Content-MD5"); } public void setContentMD5(final String md5) throws MessagingException { setHeader("Content-MD5", md5); } public String[] getContentLanguage() throws MessagingException { return getHeader("Content-Language"); } public void setContentLanguage(final String[] languages) throws MessagingException { if (languages == null) { removeHeader("Content-Language"); } else if (languages.length == 1) { setHeader("Content-Language", languages[0]); } else { final StringBuffer buf = new StringBuffer(languages.length * 20); buf.append(languages[0]); for (int i = 1; i < languages.length; i++) { buf.append(',').append(languages[i]); } setHeader("Content-Language", buf.toString()); } } public String getDescription() throws MessagingException { final String description = getSingleHeader("Content-Description"); if (description != null) { try { // this could be both folded and encoded. Return this to usable form. return MimeUtility.decodeText(MimeUtility.unfold(description)); } catch (final UnsupportedEncodingException e) { // ignore } } // return the raw version for any errors. return description; } public void setDescription(final String description) throws MessagingException { setDescription(description, null); } public void setDescription(final String description, final String charset) throws MessagingException { if (description == null) { removeHeader("Content-Description"); } else { try { setHeader("Content-Description", MimeUtility.fold(21, MimeUtility.encodeText(description, charset, null))); } catch (final UnsupportedEncodingException e) { throw new MessagingException(e.getMessage(), e); } } } public String getFileName() throws MessagingException { // see if there is a disposition. If there is, parse off the filename parameter. final String disposition = getSingleHeader("Content-Disposition"); String filename = null; if (disposition != null) { filename = new ContentDisposition(disposition).getParameter("filename"); } // if there's no filename on the disposition, there might be a name parameter on a // Content-Type header. if (filename == null) { final String type = getSingleHeader("Content-Type"); if (type != null) { try { filename = new ContentType(type).getParameter("name"); } catch (final ParseException e) { } } } // if we have a name, we might need to decode this if an additional property is set. if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) { try { filename = MimeUtility.decodeText(filename); } catch (final UnsupportedEncodingException e) { throw new MessagingException("Unable to decode filename", e); } } return filename; } public void setFileName(String name) throws MessagingException { // there's an optional session property that requests file name encoding...we need to process this before // setting the value. if (name != null && SessionUtil.getBooleanProperty(MIME_ENCODEFILENAME, false)) { try { name = MimeUtility.encodeText(name); } catch (final UnsupportedEncodingException e) { throw new MessagingException("Unable to encode filename", e); } } // get the disposition string. String disposition = getDisposition(); // if not there, then this is an attachment. if (disposition == null) { disposition = Part.ATTACHMENT; } // now create a disposition object and set the parameter. final ContentDisposition contentDisposition = new ContentDisposition(disposition); contentDisposition.setParameter("filename", name); // serialize this back out and reset. setHeader("Content-Disposition", contentDisposition.toString()); // The Sun implementation appears to update the Content-type name parameter too, based on // another system property if (SessionUtil.getBooleanProperty(MIME_SETCONTENTTYPEFILENAME, true)) { final ContentType type = new ContentType(getContentType()); type.setParameter("name", name); setHeader("Content-Type", type.toString()); } } public InputStream getInputStream() throws MessagingException, IOException { return getDataHandler().getInputStream(); } protected InputStream getContentStream() throws MessagingException { if (contentStream != null) { return contentStream; } if (content != null) { return new ByteArrayInputStream(content); } else { throw new MessagingException("No content"); } } public InputStream getRawInputStream() throws MessagingException { return getContentStream(); } public synchronized DataHandler getDataHandler() throws MessagingException { if (dh == null) { dh = new DataHandler(new MimePartDataSource(this)); } return dh; } public Object getContent() throws MessagingException, IOException { if (cachedContent != null) { return cachedContent; } final Object c = getDataHandler().getContent(); if (MimeBodyPart.cacheMultipart && (c instanceof Multipart || c instanceof Message) && (content != null || contentStream != null)) { cachedContent = c; if (c instanceof MimeMultipart) { ((MimeMultipart) c).parse(); } } return c; } public void setDataHandler(final DataHandler handler) throws MessagingException { dh = handler; // if we have a handler override, then we need to invalidate any content // headers that define the types. This information will be derived from the // data heander unless subsequently overridden. removeHeader("Content-Type"); removeHeader("Content-Transfer-Encoding"); cachedContent = null; } public void setContent(final Object content, final String type) throws MessagingException { // Multipart content needs to be handled separately. if (content instanceof Multipart) { setContent((Multipart)content); } else { setDataHandler(new DataHandler(content, type)); } } public void setText(final String text) throws MessagingException { setText(text, null); } public void setText(final String text, final String charset) throws MessagingException { // the default subtype is plain text. setText(text, charset, "plain"); } public void setText(final String text, String charset, final String subtype) throws MessagingException { // we need to sort out the character set if one is not provided. if (charset == null) { // if we have non us-ascii characters here, we need to adjust this. if (!ASCIIUtil.isAscii(text)) { charset = MimeUtility.getDefaultMIMECharset(); } else { charset = "us-ascii"; } } setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME)); } public void setContent(final Multipart part) throws MessagingException { setDataHandler(new DataHandler(part, part.getContentType())); part.setParent(this); } public void writeTo(final OutputStream out) throws IOException, MessagingException { headers.writeTo(out, null); // add the separater between the headers and the data portion. out.write('\r'); out.write('\n'); // we need to process this using the transfer encoding type final OutputStream encodingStream = MimeUtility.encode(out, getEncoding()); getDataHandler().writeTo(encodingStream); encodingStream.flush(); } public String[] getHeader(final String name) throws MessagingException { return headers.getHeader(name); } public String getHeader(final String name, final String delimiter) throws MessagingException { return headers.getHeader(name, delimiter); } public void setHeader(final String name, final String value) throws MessagingException { headers.setHeader(name, value); } /** * Conditionally set or remove a named header. If the new value * is null, the header is removed. * * @param name The header name. * @param value The new header value. A null value causes the header to be * removed. * * @exception MessagingException */ private void setOrRemoveHeader(final String name, final String value) throws MessagingException { if (value == null) { headers.removeHeader(name); } else { headers.setHeader(name, value); } } public void addHeader(final String name, final String value) throws MessagingException { headers.addHeader(name, value); } public void removeHeader(final String name) throws MessagingException { headers.removeHeader(name); } public Enumeration getAllHeaders() throws MessagingException { return headers.getAllHeaders(); } public Enumeration getMatchingHeaders(final String[] name) throws MessagingException { return headers.getMatchingHeaders(name); } public Enumeration getNonMatchingHeaders(final String[] name) throws MessagingException { return headers.getNonMatchingHeaders(name); } public void addHeaderLine(final String line) throws MessagingException { headers.addHeaderLine(line); } public Enumeration getAllHeaderLines() throws MessagingException { return headers.getAllHeaderLines(); } public Enumeration getMatchingHeaderLines(final String[] names) throws MessagingException { return headers.getMatchingHeaderLines(names); } public Enumeration getNonMatchingHeaderLines(final String[] names) throws MessagingException { return headers.getNonMatchingHeaderLines(names); } protected void updateHeaders() throws MessagingException { final DataHandler handler = getDataHandler(); try { // figure out the content type. If not set, we'll need to figure this out. String type = dh.getContentType(); // parse this content type out so we can do matches/compares. final ContentType contentType = new ContentType(type); // we might need to reconcile the content type and our explicitly set type final String explicitType = getSingleHeader("Content-Type"); // is this a multipart content? if (contentType.match("multipart/*")) { // the content is suppose to be a MimeMultipart. Ping it to update it's headers as well. try { final MimeMultipart part = (MimeMultipart)handler.getContent(); part.updateHeaders(); } catch (final ClassCastException e) { throw new MessagingException("Message content is not MimeMultipart", e); } } else if (!contentType.match("message/rfc822")) { // simple part, we need to update the header type information // if no encoding is set yet, figure this out from the data handler. if (getSingleHeader("Content-Transfer-Encoding") == null) { setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler)); } // is a content type header set? Check the property to see if we need to set this. if (explicitType == null) { if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) { // is this a text type? Figure out the encoding and make sure it is set. if (contentType.match("text/*")) { // the charset should be specified as a parameter on the MIME type. If not there, // try to figure one out. if (contentType.getParameter("charset") == null) { final String encoding = getEncoding(); // if we're sending this as 7-bit ASCII, our character set need to be // compatible. if (encoding != null && encoding.equalsIgnoreCase("7bit")) { contentType.setParameter("charset", "us-ascii"); } else { // get the global default. contentType.setParameter("charset", MimeUtility.getDefaultMIMECharset()); } // replace the datasource provided type type = contentType.toString(); } } } } } // if we don't have a content type header, then create one. if (explicitType == null) { // get the disposition header, and if it is there, copy the filename parameter into the // name parameter of the type. final String disp = getHeader("Content-Disposition", null); if (disp != null) { // parse up the string value of the disposition final ContentDisposition disposition = new ContentDisposition(disp); // now check for a filename value final String filename = disposition.getParameter("filename"); // copy and rename the parameter, if it exists. if (filename != null) { contentType.setParameter("name", filename); // and update the string version type = contentType.toString(); } } // set the header with the updated content type information. setHeader("Content-Type", type); } if (cachedContent != null) { dh = new DataHandler(cachedContent, getContentType()); cachedContent = null; content = null; if (contentStream != null) { try { contentStream.close(); } catch (final IOException ioex) { //np-op } } contentStream = null; } } catch (final IOException e) { throw new MessagingException("Error updating message headers", e); } } private String getSingleHeader(final String name) throws MessagingException { final String[] values = getHeader(name); if (values == null || values.length == 0) { return null; } else { return values[0]; } } /** * Use the specified file to provide the data for this part. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The encoding will be chosen appropriately for the * file data. The disposition of this part is set to * {@link Part#ATTACHMENT Part.ATTACHMENT}. * * @param file the File object to attach * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.4 */ public void attachFile(final File file) throws IOException, MessagingException { final FileDataSource dataSource = new FileDataSource(file); setDataHandler(new DataHandler(dataSource)); setFileName(dataSource.getName()); /* Since JavaMail 1.5: An oversight when these methods were originally added. Clearly attachments should set the disposition to ATTACHMENT. */ setDisposition(ATTACHMENT); } /** * Use the specified file to provide the data for this part. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The encoding will be chosen appropriately for the * file data. * * @param file the name of the file to attach * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.4 */ public void attachFile(final String file) throws IOException, MessagingException { attachFile(new File(file)); } /** * Use the specified file with the specified Content-Type and * Content-Transfer-Encoding to provide the data for this part. * If contentType or encoding are null, appropriate values will * be chosen. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The disposition of this part is set to * {@link Part#ATTACHMENT Part.ATTACHMENT}. * * @param file the File object to attach * @param contentType the Content-Type, or null * @param encoding the Content-Transfer-Encoding, or null * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.5 */ public void attachFile(final File file, final String contentType, final String encoding) throws IOException, MessagingException { final FileDataSource dataSource = new EncodingAwareFileDataSource(file, contentType, encoding); setDataHandler(new DataHandler(dataSource)); setFileName(dataSource.getName()); /* Since JavaMail 1.5: An oversight when these methods were originally added. Clearly attachments should set the disposition to ATTACHMENT. */ setDisposition(ATTACHMENT); } /** * Use the specified file with the specified Content-Type and * Content-Transfer-Encoding to provide the data for this part. * If contentType or encoding are null, appropriate values will * be chosen. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The disposition of this part is set to * {@link Part#ATTACHMENT Part.ATTACHMENT}. * * @param file the name of the file * @param contentType the Content-Type, or null * @param encoding the Content-Transfer-Encoding, or null * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.5 */ public void attachFile(final String file, final String contentType, final String encoding) throws IOException, MessagingException { attachFile(new File(file), contentType, encoding); } /** * Save the body part content to a given target file. * * @param file The File object used to store the information. * * @exception IOException * @exception MessagingException */ public void saveFile(final File file) throws IOException, MessagingException { final OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); // we need to read the data in to write it out (sigh). final InputStream in = getInputStream(); try { final byte[] buffer = new byte[8192]; int length; while ((length = in.read(buffer)) > 0) { out.write(buffer, 0, length); } } finally { // make sure all of the streams are closed before we return if (in != null) { in.close(); } if (out != null) { out.close(); } } } /** * Save the body part content to a given target file. * * @param file The file name used to store the information. * * @exception IOException * @exception MessagingException */ public void saveFile(final String file) throws IOException, MessagingException { saveFile(new File(file)); } private static class EncodingAwareFileDataSource extends FileDataSource implements EncodingAware { private final String contentType; private final String encoding; public EncodingAwareFileDataSource(final File file, final String contentType, final String encoding) { super(file); this.contentType = contentType; this.encoding = encoding; } @Override public String getContentType() { return contentType == null ? super.getContentType() : contentType; } //this will be evaluated in MimeUtility.getEncoding(DataSource) public String getEncoding() { return encoding; } } }