/* * 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; } }