blob: 7bc84038c8337e2a47ce61d54357c394c21670c0 [file] [log] [blame]
/*
* 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;
}
}
}