blob: 232da8d9a78d2dea9d470ba7f48c5b67a83220d0 [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.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import javax.activation.DataSource;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.MultipartDataSource;
import org.apache.geronimo.mail.util.SessionUtil;
/**
* @version $Rev$ $Date$
*/
public class MimeMultipart extends Multipart {
private static final String MIME_IGNORE_MISSING_ENDBOUNDARY = "mail.mime.multipart.ignoremissingendboundary";
private static final String MIME_IGNORE_MISSING_BOUNDARY_PARAMETER = "mail.mime.multipart.ignoremissingboundaryparameter";
private static final String MIME_IGNORE_EXISTING_BOUNDARY_PARAMETER = "mail.mime.multipart.ignoreexistingboundaryparameter";
private static final String MIME_ALLOWEMPTY = "mail.mime.multipart.allowempty";
/**
* DataSource that provides our InputStream.
*/
protected DataSource ds;
/**
* Indicates if the data has been parsed.
*/
protected boolean parsed = true;
// the content type information
private transient ContentType type;
/** Have we seen the final bounary line?
*
* @since JavaMail 1.5
*/
protected boolean complete = true;
/**
* The MIME multipart preamble text, the text that
* occurs before the first boundary line.
*
* @since JavaMail 1.5
*/
protected String preamble = null;
/**
* Flag corresponding to the "mail.mime.multipart.ignoremissingendboundary"
* property, set in the {@link #initializeProperties} method called from
* constructors and the parse method.
*
* @since JavaMail 1.5
*/
protected boolean ignoreMissingEndBoundary = true;
/**
* Flag corresponding to the
* "mail.mime.multipart.ignoremissingboundaryparameter"
* property, set in the {@link #initializeProperties} method called from
* constructors and the parse method.
*
* @since JavaMail 1.5
*/
protected boolean ignoreMissingBoundaryParameter = true;
/**
* Flag corresponding to the
* "mail.mime.multipart.ignoreexistingboundaryparameter"
* property, set in the {@link #initializeProperties} method called from
* constructors and the parse method.
*
* @since JavaMail 1.5
*/
protected boolean ignoreExistingBoundaryParameter = false;
/**
* Flag corresponding to the "mail.mime.multipart.allowempty"
* property, set in the {@link #initializeProperties} method called from
* constructors and the parse method.
*
* @since JavaMail 1.5
*/
protected boolean allowEmpty = false;
/**
* Initialize flags that control parsing behavior,
* based on System properties described above in
* the class documentation.
*
* @since JavaMail 1.5
*/
protected void initializeProperties() {
ignoreMissingEndBoundary = SessionUtil.getBooleanProperty(MIME_IGNORE_MISSING_ENDBOUNDARY, true);
ignoreMissingBoundaryParameter = SessionUtil.getBooleanProperty(MIME_IGNORE_MISSING_BOUNDARY_PARAMETER, true);
ignoreExistingBoundaryParameter = SessionUtil.getBooleanProperty(MIME_IGNORE_EXISTING_BOUNDARY_PARAMETER, false);
allowEmpty = SessionUtil.getBooleanProperty(MIME_ALLOWEMPTY, false);
}
/**
* Create an empty MimeMultipart with content type "multipart/mixed"
*/
public MimeMultipart() {
this("mixed");
}
/**
* Create an empty MimeMultipart with the subtype supplied.
*
* @param subtype the subtype
*/
public MimeMultipart(final String subtype) {
type = new ContentType("multipart", subtype, null);
type.setParameter("boundary", getBoundary());
contentType = type.toString();
initializeProperties();
}
/**
* Create a MimeMultipart from the supplied DataSource.
*
* @param dataSource the DataSource to use
* @throws MessagingException
*/
public MimeMultipart(final DataSource dataSource) throws MessagingException {
ds = dataSource;
if (dataSource instanceof MultipartDataSource) {
super.setMultipartDataSource((MultipartDataSource) dataSource);
parsed = true;
} else {
// We keep the original, provided content type string so that we
// don't end up changing quoting/formatting of the header unless
// changes are made to the content type. James is somewhat dependent
// on that behavior.
contentType = ds.getContentType();
type = new ContentType(contentType);
parsed = false;
}
}
/**
* Construct a MimeMultipart object of the default "mixed" subtype,
* and with the given body parts. More body parts may be added later.
*
* @since JavaMail 1.5
*/
public MimeMultipart(final BodyPart... parts) throws MessagingException {
this("mixed");
this.parts.addAll(Arrays.asList(parts));
}
/**
* Construct a MimeMultipart object of the given subtype
* and with the given body parts. More body parts may be added later.
*
* @since JavaMail 1.5
*/
public MimeMultipart(final String subtype, final BodyPart... parts)
throws MessagingException {
this(subtype);
this.parts.addAll(Arrays.asList(parts));
}
public void setSubType(final String subtype) throws MessagingException {
type.setSubType(subtype);
contentType = type.toString();
}
@Override
public int getCount() throws MessagingException {
parse();
return super.getCount();
}
@Override
public synchronized BodyPart getBodyPart(final int part) throws MessagingException {
parse();
return super.getBodyPart(part);
}
public BodyPart getBodyPart(final String cid) throws MessagingException {
parse();
for (int i = 0; i < parts.size(); i++) {
final MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
if (cid.equals(bodyPart.getContentID())) {
return bodyPart;
}
}
return null;
}
protected void updateHeaders() throws MessagingException {
parse();
for (int i = 0; i < parts.size(); i++) {
final MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
bodyPart.updateHeaders();
}
}
private static byte[] dash = { '-', '-' };
private static byte[] crlf = { 13, 10 };
@Override
public void writeTo(final OutputStream out) throws IOException, MessagingException {
parse();
final String boundary = type.getParameter("boundary");
final byte[] bytes = boundary.getBytes("ISO8859-1");
if(!allowEmpty && parts.size() == 0) {
throw new MessagingException("Multipart content with no body parts is not allowed");
}
if (preamble != null) {
final byte[] preambleBytes = preamble.getBytes("ISO8859-1");
// write this out, followed by a line break.
out.write(preambleBytes);
out.write(crlf);
}
for (int i = 0; i < parts.size(); i++) {
final BodyPart bodyPart = (BodyPart) parts.get(i);
out.write(dash);
out.write(bytes);
out.write(crlf);
bodyPart.writeTo(out);
out.write(crlf);
}
out.write(dash);
out.write(bytes);
out.write(dash);
out.write(crlf);
out.flush();
}
protected void parse() throws MessagingException {
if (parsed) {
return;
}
initializeProperties();
try {
final ContentType cType = new ContentType(contentType);
final String boundaryString = cType.getParameter("boundary");
if(!ignoreMissingBoundaryParameter && boundaryString == null) {
throw new MessagingException("Missing boundary parameter in content-type");
}
final InputStream is = new BufferedInputStream(ds.getInputStream());
BufferedInputStream pushbackInStream = null;
boolean boundaryFound = false;
byte[] boundary = null;
if (boundaryString == null || ignoreExistingBoundaryParameter) {
pushbackInStream = new BufferedInputStream(is, 1200);
// read until we find something that looks like a boundary string
boundary = readTillFirstBoundary(pushbackInStream);
boundaryFound = boundary != null;
}
else {
boundary = ("--" + boundaryString).getBytes("ISO8859-1");
pushbackInStream = new BufferedInputStream(is, boundary.length + 1000);
boundaryFound = readTillFirstBoundary(pushbackInStream, boundary);
}
if(allowEmpty && !boundaryFound) {
parsed = true;
return;
}
if(!allowEmpty && !boundaryFound) {
throw new MessagingException("Multipart content with no body parts is not allowed");
}
while (true) {
MimeBodyPartInputStream partStream;
partStream = new MimeBodyPartInputStream(pushbackInStream, boundary);
addBodyPart(new MimeBodyPart(partStream));
// terminated by an EOF rather than a proper boundary?
if (!partStream.boundaryFound) {
if (!ignoreMissingEndBoundary) {
throw new MessagingException("Missing Multi-part end boundary");
}
complete = false;
break;
}
// if we hit the final boundary, stop processing this
if (partStream.finalBoundaryFound) {
break;
}
}
} catch (final Exception e){
throw new MessagingException(e.toString(),e);
}
parsed = true;
}
/**
* Move the read pointer to the beginning of the first part
* read till the end of first boundary. Any data read before this point are
* saved as the preamble.
*
* @param pushbackInStream
* @param boundary
* @throws MessagingException
*/
private byte[] readTillFirstBoundary(final BufferedInputStream pushbackInStream) throws MessagingException {
final ByteArrayOutputStream preambleStream = new ByteArrayOutputStream();
try {
while (true) {
// read the next line
final byte[] line = readLine(pushbackInStream);
// hit an EOF?
if (line == null || line.length==0) {
return null;//throw new MessagingException("Unexpected End of Stream while searching for first Mime Boundary");
}
// if this looks like a boundary, then make it so
if (line.length > 2 && line[0] == '-' && line[1] == '-') {
// save the preamble, if there is one.
final byte[] preambleBytes = preambleStream.toByteArray();
if (preambleBytes.length > 0) {
preamble = new String(preambleBytes, "ISO8859-1");
}
return stripLinearWhiteSpace(line);
}
else {
// this is part of the preamble.
preambleStream.write(line);
preambleStream.write('\r');
preambleStream.write('\n');
}
}
} catch (final IOException ioe) {
throw new MessagingException(ioe.toString(), ioe);
}
}
/**
* Scan a line buffer stripping off linear whitespace
* characters, returning a new array without the
* characters, if possible.
*
* @param line The source line buffer.
*
* @return A byte array with white space characters removed,
* if necessary.
*/
private byte[] stripLinearWhiteSpace(final byte[] line) {
int index = line.length - 1;
// if the last character is not a space or tab, we
// can use this unchanged
if (line[index] != ' ' && line[index] != '\t') {
return line;
}
// scan backwards for the first non-white space
for (; index > 0; index--) {
if (line[index] != ' ' && line[index] != '\t') {
break;
}
}
// make a shorter copy of this
final byte[] newLine = new byte[index + 1];
System.arraycopy(line, 0, newLine, 0, index + 1);
return newLine;
}
/**
* Move the read pointer to the beginning of the first part
* read till the end of first boundary. Any data read before this point are
* saved as the preamble.
*
* @param pushbackInStream
* @param boundary
* @throws MessagingException
*/
private boolean readTillFirstBoundary(final BufferedInputStream pushbackInStream, final byte[] boundary) throws MessagingException {
final ByteArrayOutputStream preambleStream = new ByteArrayOutputStream();
try {
while (true) {
// read the next line
final byte[] line = readLine(pushbackInStream);
// hit an EOF?
if (line == null || line.length==0) {
return false;//throw new MessagingException("Unexpected End of Stream while searching for first Mime Boundary");
}
// apply the boundary comparison rules to this
if (compareBoundary(line, boundary)) {
// save the preamble, if there is one.
final byte[] preambleBytes = preambleStream.toByteArray();
if (preambleBytes.length > 0) {
preamble = new String(preambleBytes, "ISO8859-1");
}
return true;
}
// this is part of the preamble.
preambleStream.write(line);
preambleStream.write('\r');
preambleStream.write('\n');
}
} catch (final IOException ioe) {
throw new MessagingException(ioe.toString(), ioe);
}
}
/**
* Perform a boundary comparison, taking into account
* potential linear white space
*
* @param line The line to compare.
* @param boundary The boundary we're searching for
*
* @return true if this is a valid boundary line, false for
* any mismatches.
*/
private boolean compareBoundary(final byte[] line, final byte[] boundary) {
// if the line is too short, this is an easy failure
if (line.length < boundary.length) {
return false;
}
// this is the most common situation
if (line.length == boundary.length) {
return Arrays.equals(line, boundary);
}
// the line might have linear white space after the boundary portions
for (int i = 0; i < boundary.length; i++) {
// fail on any mismatch
if (line[i] != boundary[i]) {
return false;
}
}
// everything after the boundary portion must be linear whitespace
for (int i = boundary.length; i < line.length; i++) {
// fail on any mismatch
if (line[i] != ' ' && line[i] != '\t') {
return false;
}
}
// these are equivalent
return true;
}
/**
* Read a single line of data from the input stream,
* returning it as an array of bytes.
*
* @param in The source input stream.
*
* @return A byte array containing the line data. Returns
* null if there's nothing left in the stream.
* @exception MessagingException
*/
private byte[] readLine(final BufferedInputStream in) throws IOException
{
final ByteArrayOutputStream line = new ByteArrayOutputStream();
while (in.available() > 0) {
int value = in.read();
if (value == -1) {
// if we have nothing in the accumulator, signal an EOF back
if (line.size() == 0) {
return null;
}
break;
}
else if (value == '\r') {
in.mark(10);
value = in.read();
// we expect to find a linefeed after the carriage return, but
// some things play loose with the rules.
if (value != '\n') {
in.reset();
}
break;
}
else if (value == '\n') {
// naked linefeed, allow that
break;
}
else {
// write this to the line
line.write((byte)value);
}
}
// return this as an array of bytes
return line.toByteArray();
}
protected InternetHeaders createInternetHeaders(final InputStream in) throws MessagingException {
return new InternetHeaders(in);
}
protected MimeBodyPart createMimeBodyPart(final InternetHeaders headers, final byte[] data) throws MessagingException {
return new MimeBodyPart(headers, data);
}
protected MimeBodyPart createMimeBodyPart(final InputStream in) throws MessagingException {
return new MimeBodyPart(in);
}
// static used to track boundary value allocations to help ensure uniqueness.
private static int part;
private synchronized static String getBoundary() {
int i;
synchronized(MimeMultipart.class) {
i = part++;
}
final StringBuffer buf = new StringBuffer(64);
buf.append("----=_Part_").append(i).append('_').append((new Object()).hashCode()).append('.').append(System.currentTimeMillis());
return buf.toString();
}
private class MimeBodyPartInputStream extends InputStream {
BufferedInputStream inStream;
public boolean boundaryFound = false;
byte[] boundary;
public boolean finalBoundaryFound = false;
public MimeBodyPartInputStream(final BufferedInputStream inStream, final byte[] boundary) {
super();
this.inStream = inStream;
this.boundary = boundary;
}
/**
* The base reading method for reading one character
* at a time.
*
* @return The read character, or -1 if an EOF was encountered.
* @exception IOException
*/
@Override
public int read() throws IOException {
if (boundaryFound) {
return -1;
}
// read the next value from stream
final int firstChar = inStream.read();
// premature end? Handle it like a boundary located
if (firstChar == -1) {
//DO NOT treat this a a boundary because if we do so we have no chance to detect missing end boundaries
return -1;
}
// we first need to look for a line boundary. If we find a boundary, it can be followed by the
// boundary marker, so we need to remember what sort of thing we found, then read ahead looking
// for the part boundary.
// NB:, we only handle [\r]\n--boundary marker[--]
// we need to at least accept what most mail servers would consider an
// invalid format using just '\n'
if (firstChar != '\r' && firstChar != '\n') {
// not a \r, just return the byte as is
return firstChar;
}
// we might need to rewind to this point. The padding is to allow for
// line terminators and linear whitespace on the boundary lines
inStream.mark(boundary.length + 1000);
// we need to keep track of the first read character in case we need to
// rewind back to the mark point
int value = firstChar;
// if this is a '\r', then we require the '\n'
if (value == '\r') {
// now scan ahead for the second character
value = inStream.read();
if (value != '\n') {
// only a \r, so this can't be a boundary. Return the
// \r as if it was data, after first resetting
inStream.reset();
return '\r';
}
}
value = inStream.read();
// if the next character is not a boundary start, we
// need to handle this as a normal line end
if ((byte) value != boundary[0]) {
// just reset and return the first character as data
inStream.reset();
return firstChar;
}
// we're here because we found a "\r\n-" sequence, which is a potential
// boundary marker. Read the individual characters of the next line until
// we have a mismatch
// read value is the first byte of the boundary. Start matching the
// next characters to find a boundary
int boundaryIndex = 0;
while ((boundaryIndex < boundary.length) && ((byte) value == boundary[boundaryIndex])) {
value = inStream.read();
boundaryIndex++;
}
// if we didn't match all the way, we need to push back what we've read and
// return the EOL character
if (boundaryIndex != boundary.length) {
// Boundary not found. Restoring bytes skipped.
// just reset and return the first character as data
inStream.reset();
return firstChar;
}
// The full boundary sequence should be \r\n--boundary string[--]\r\n
// if the last character we read was a '-', check for the end terminator
if (value == '-') {
value = inStream.read();
// crud, we have a bad boundary terminator. We need to unwind this all the way
// back to the lineend and pretend none of this ever happened
if (value != '-') {
// Boundary not found. Restoring bytes skipped.
// just reset and return the first character as data
inStream.reset();
return firstChar;
}
// on the home stretch, but we need to verify the LWSP/EOL sequence
value = inStream.read();
// first skip over the linear whitespace
while (value == ' ' || value == '\t') {
value = inStream.read();
}
// We've matched the final boundary, skipped any whitespace, but
// we've hit the end of the stream. This is highly likely when
// we have nested multiparts, since the linend terminator for the
// final boundary marker is eated up as the start of the outer
// boundary marker. No CRLF sequence here is ok.
if (value == -1) {
// we've hit the end of times...
finalBoundaryFound = true;
// we have a boundary, so return this as an EOF condition
boundaryFound = true;
return -1;
}
// this must be a CR or a LF...which leaves us even more to push back and forget
if (value != '\r' && value != '\n') {
// Boundary not found. Restoring bytes skipped.
// just reset and return the first character as data
inStream.reset();
return firstChar;
}
// if this is carriage return, check for a linefeed
if (value == '\r') {
// last check, this must be a line feed
value = inStream.read();
if (value != '\n') {
// SO CLOSE!
// Boundary not found. Restoring bytes skipped.
// just reset and return the first character as data
inStream.reset();
return firstChar;
}
}
// we've hit the end of times...
finalBoundaryFound = true;
}
else {
// first skip over the linear whitespace
while (value == ' ' || value == '\t') {
value = inStream.read();
}
// this must be a CR or a LF...which leaves us even more to push back and forget
if (value != '\r' && value != '\n') {
// Boundary not found. Restoring bytes skipped.
// just reset and return the first character as data
inStream.reset();
return firstChar;
}
// if this is carriage return, check for a linefeed
if (value == '\r') {
// last check, this must be a line feed
value = inStream.read();
if (value != '\n') {
// SO CLOSE!
// Boundary not found. Restoring bytes skipped.
// just reset and return the first character as data
inStream.reset();
return firstChar;
}
}
}
// we have a boundary, so return this as an EOF condition
boundaryFound = true;
return -1;
}
}
/**
* Return true if the final boundary line for this multipart was
* seen when parsing the data.
*
* @return
* @exception MessagingException
*/
public boolean isComplete() throws MessagingException {
// make sure we've parsed this
parse();
return complete;
}
/**
* Returns the preamble text that appears before the first bady
* part of a MIME multi part. The preamble is optional, so this
* might be null.
*
* @return The preamble text string.
* @exception MessagingException
*/
public String getPreamble() throws MessagingException {
parse();
return preamble;
}
/**
* Set the message preamble text. This will be written before
* the first boundary of a multi-part message.
*
* @param preamble The new boundary text. This is complete lines of text, including
* new lines.
*
* @exception MessagingException
*/
public void setPreamble(final String preamble) throws MessagingException {
this.preamble = preamble;
}
}