001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020 package javax.mail.internet;
021
022 import java.io.BufferedInputStream;
023 import java.io.ByteArrayInputStream;
024 import java.io.ByteArrayOutputStream;
025 import java.io.IOException;
026 import java.io.InputStream;
027 import java.io.OutputStream;
028
029 import java.util.Arrays;
030
031 import javax.activation.DataSource;
032 import javax.mail.BodyPart;
033 import javax.mail.MessagingException;
034 import javax.mail.Multipart;
035 import javax.mail.MultipartDataSource;
036
037 import org.apache.geronimo.mail.util.SessionUtil;
038
039 /**
040 * @version $Rev: 689486 $ $Date: 2008-08-27 10:11:03 -0400 (Wed, 27 Aug 2008) $
041 */
042 public class MimeMultipart extends Multipart {
043 private static final String MIME_IGNORE_MISSING_BOUNDARY = "mail.mime.multipart.ignoremissingendboundary";
044
045 /**
046 * DataSource that provides our InputStream.
047 */
048 protected DataSource ds;
049 /**
050 * Indicates if the data has been parsed.
051 */
052 protected boolean parsed = true;
053
054 // the content type information
055 private transient ContentType type;
056
057 // indicates if we've seen the final boundary line when parsing.
058 private boolean complete = true;
059
060 // MIME multipart preable text that can appear before the first boundary line.
061 private String preamble = null;
062
063 /**
064 * Create an empty MimeMultipart with content type "multipart/mixed"
065 */
066 public MimeMultipart() {
067 this("mixed");
068 }
069
070 /**
071 * Create an empty MimeMultipart with the subtype supplied.
072 *
073 * @param subtype the subtype
074 */
075 public MimeMultipart(String subtype) {
076 type = new ContentType("multipart", subtype, null);
077 type.setParameter("boundary", getBoundary());
078 contentType = type.toString();
079 }
080
081 /**
082 * Create a MimeMultipart from the supplied DataSource.
083 *
084 * @param dataSource the DataSource to use
085 * @throws MessagingException
086 */
087 public MimeMultipart(DataSource dataSource) throws MessagingException {
088 ds = dataSource;
089 if (dataSource instanceof MultipartDataSource) {
090 super.setMultipartDataSource((MultipartDataSource) dataSource);
091 parsed = true;
092 } else {
093 // We keep the original, provided content type string so that we
094 // don't end up changing quoting/formatting of the header unless
095 // changes are made to the content type. James is somewhat dependent
096 // on that behavior.
097 contentType = ds.getContentType();
098 type = new ContentType(contentType);
099 parsed = false;
100 }
101 }
102
103 public void setSubType(String subtype) throws MessagingException {
104 type.setSubType(subtype);
105 contentType = type.toString();
106 }
107
108 public int getCount() throws MessagingException {
109 parse();
110 return super.getCount();
111 }
112
113 public synchronized BodyPart getBodyPart(int part) throws MessagingException {
114 parse();
115 return super.getBodyPart(part);
116 }
117
118 public BodyPart getBodyPart(String cid) throws MessagingException {
119 parse();
120 for (int i = 0; i < parts.size(); i++) {
121 MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
122 if (cid.equals(bodyPart.getContentID())) {
123 return bodyPart;
124 }
125 }
126 return null;
127 }
128
129 protected void updateHeaders() throws MessagingException {
130 parse();
131 for (int i = 0; i < parts.size(); i++) {
132 MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
133 bodyPart.updateHeaders();
134 }
135 }
136
137 private static byte[] dash = { '-', '-' };
138 private static byte[] crlf = { 13, 10 };
139
140 public void writeTo(OutputStream out) throws IOException, MessagingException {
141 parse();
142 String boundary = type.getParameter("boundary");
143 byte[] bytes = boundary.getBytes();
144
145 if (preamble != null) {
146 byte[] preambleBytes = preamble.getBytes();
147 // write this out, followed by a line break.
148 out.write(preambleBytes);
149 out.write(crlf);
150 }
151
152 for (int i = 0; i < parts.size(); i++) {
153 BodyPart bodyPart = (BodyPart) parts.get(i);
154 out.write(dash);
155 out.write(bytes);
156 out.write(crlf);
157 bodyPart.writeTo(out);
158 out.write(crlf);
159 }
160 out.write(dash);
161 out.write(bytes);
162 out.write(dash);
163 out.write(crlf);
164 out.flush();
165 }
166
167 protected void parse() throws MessagingException {
168 if (parsed) {
169 return;
170 }
171
172 try {
173 ContentType cType = new ContentType(contentType);
174 InputStream is = new BufferedInputStream(ds.getInputStream());
175 BufferedInputStream pushbackInStream = null;
176 String boundaryString = cType.getParameter("boundary");
177 byte[] boundary = null;
178 if (boundaryString == null) {
179 pushbackInStream = new BufferedInputStream(is, 1200);
180 // read until we find something that looks like a boundary string
181 boundary = readTillFirstBoundary(pushbackInStream);
182 }
183 else {
184 boundary = ("--" + boundaryString).getBytes();
185 pushbackInStream = new BufferedInputStream(is, boundary.length + 1000);
186 readTillFirstBoundary(pushbackInStream, boundary);
187 }
188
189 while (true) {
190 MimeBodyPartInputStream partStream;
191 partStream = new MimeBodyPartInputStream(pushbackInStream, boundary);
192 addBodyPart(new MimeBodyPart(partStream));
193
194 // terminated by an EOF rather than a proper boundary?
195 if (!partStream.boundaryFound) {
196 if (!SessionUtil.getBooleanProperty(MIME_IGNORE_MISSING_BOUNDARY, true)) {
197 throw new MessagingException("Missing Multi-part end boundary");
198 }
199 complete = false;
200 }
201 // if we hit the final boundary, stop processing this
202 if (partStream.finalBoundaryFound) {
203 break;
204 }
205 }
206 } catch (Exception e){
207 throw new MessagingException(e.toString(),e);
208 }
209 parsed = true;
210 }
211
212 /**
213 * Move the read pointer to the begining of the first part
214 * read till the end of first boundary. Any data read before this point are
215 * saved as the preamble.
216 *
217 * @param pushbackInStream
218 * @param boundary
219 * @throws MessagingException
220 */
221 private byte[] readTillFirstBoundary(BufferedInputStream pushbackInStream) throws MessagingException {
222 ByteArrayOutputStream preambleStream = new ByteArrayOutputStream();
223
224 try {
225 while (true) {
226 // read the next line
227 byte[] line = readLine(pushbackInStream);
228 // hit an EOF?
229 if (line == null) {
230 throw new MessagingException("Unexpected End of Stream while searching for first Mime Boundary");
231 }
232 // if this looks like a boundary, then make it so
233 if (line.length > 2 && line[0] == '-' && line[1] == '-') {
234 // save the preamble, if there is one.
235 byte[] preambleBytes = preambleStream.toByteArray();
236 if (preambleBytes.length > 0) {
237 preamble = new String(preambleBytes);
238 }
239 return stripLinearWhiteSpace(line);
240 }
241 else {
242 // this is part of the preamble.
243 preambleStream.write(line);
244 preambleStream.write('\r');
245 preambleStream.write('\n');
246 }
247 }
248 } catch (IOException ioe) {
249 throw new MessagingException(ioe.toString(), ioe);
250 }
251 }
252
253
254 /**
255 * Scan a line buffer stripping off linear whitespace
256 * characters, returning a new array without the
257 * characters, if possible.
258 *
259 * @param line The source line buffer.
260 *
261 * @return A byte array with white space characters removed,
262 * if necessary.
263 */
264 private byte[] stripLinearWhiteSpace(byte[] line) {
265 int index = line.length - 1;
266 // if the last character is not a space or tab, we
267 // can use this unchanged
268 if (line[index] != ' ' && line[index] != '\t') {
269 return line;
270 }
271 // scan backwards for the first non-white space
272 for (; index > 0; index--) {
273 if (line[index] != ' ' && line[index] != '\t') {
274 break;
275 }
276 }
277 // make a shorter copy of this
278 byte[] newLine = new byte[index + 1];
279 System.arraycopy(line, 0, newLine, 0, index + 1);
280 return newLine;
281 }
282
283 /**
284 * Move the read pointer to the begining of the first part
285 * read till the end of first boundary. Any data read before this point are
286 * saved as the preamble.
287 *
288 * @param pushbackInStream
289 * @param boundary
290 * @throws MessagingException
291 */
292 private void readTillFirstBoundary(BufferedInputStream pushbackInStream, byte[] boundary) throws MessagingException {
293 ByteArrayOutputStream preambleStream = new ByteArrayOutputStream();
294
295 try {
296 while (true) {
297 // read the next line
298 byte[] line = readLine(pushbackInStream);
299 // hit an EOF?
300 if (line == null) {
301 throw new MessagingException("Unexpected End of Stream while searching for first Mime Boundary");
302 }
303
304 // apply the boundary comparison rules to this
305 if (compareBoundary(line, boundary)) {
306 // save the preamble, if there is one.
307 byte[] preambleBytes = preambleStream.toByteArray();
308 if (preambleBytes.length > 0) {
309 preamble = new String(preambleBytes);
310 }
311 return;
312 }
313
314 // this is part of the preamble.
315 preambleStream.write(line);
316 preambleStream.write('\r');
317 preambleStream.write('\n');
318 }
319 } catch (IOException ioe) {
320 throw new MessagingException(ioe.toString(), ioe);
321 }
322 }
323
324
325 /**
326 * Peform a boundary comparison, taking into account
327 * potential linear white space
328 *
329 * @param line The line to compare.
330 * @param boundary The boundary we're searching for
331 *
332 * @return true if this is a valid boundary line, false for
333 * any mismatches.
334 */
335 private boolean compareBoundary(byte[] line, byte[] boundary) {
336 // if the line is too short, this is an easy failure
337 if (line.length < boundary.length) {
338 return false;
339 }
340
341 // this is the most common situation
342 if (line.length == boundary.length) {
343 return Arrays.equals(line, boundary);
344 }
345 // the line might have linear white space after the boundary portions
346 for (int i = 0; i < boundary.length; i++) {
347 // fail on any mismatch
348 if (line[i] != boundary[i]) {
349 return false;
350 }
351 }
352 // everything after the boundary portion must be linear whitespace
353 for (int i = boundary.length; i < line.length; i++) {
354 // fail on any mismatch
355 if (line[i] != ' ' && line[i] != '\t') {
356 return false;
357 }
358 }
359 // these are equivalent
360 return true;
361 }
362
363 /**
364 * Read a single line of data from the input stream,
365 * returning it as an array of bytes.
366 *
367 * @param in The source input stream.
368 *
369 * @return A byte array containing the line data. Returns
370 * null if there's nothing left in the stream.
371 * @exception MessagingException
372 */
373 private byte[] readLine(BufferedInputStream in) throws IOException
374 {
375 ByteArrayOutputStream line = new ByteArrayOutputStream();
376
377 while (in.available() > 0) {
378 int value = in.read();
379 if (value == -1) {
380 // if we have nothing in the accumulator, signal an EOF back
381 if (line.size() == 0) {
382 return null;
383 }
384 break;
385 }
386 else if (value == '\r') {
387 in.mark(10);
388 value = in.read();
389 // we expect to find a linefeed after the carriage return, but
390 // some things play loose with the rules.
391 if (value != '\n') {
392 in.reset();
393 }
394 break;
395 }
396 else if (value == '\n') {
397 // naked linefeed, allow that
398 break;
399 }
400 else {
401 // write this to the line
402 line.write((byte)value);
403 }
404 }
405 // return this as an array of bytes
406 return line.toByteArray();
407 }
408
409
410 protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException {
411 return new InternetHeaders(in);
412 }
413
414 protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, byte[] data) throws MessagingException {
415 return new MimeBodyPart(headers, data);
416 }
417
418 protected MimeBodyPart createMimeBodyPart(InputStream in) throws MessagingException {
419 return new MimeBodyPart(in);
420 }
421
422 // static used to track boudary value allocations to help ensure uniqueness.
423 private static int part;
424
425 private synchronized static String getBoundary() {
426 int i;
427 synchronized(MimeMultipart.class) {
428 i = part++;
429 }
430 StringBuffer buf = new StringBuffer(64);
431 buf.append("----=_Part_").append(i).append('_').append((new Object()).hashCode()).append('.').append(System.currentTimeMillis());
432 return buf.toString();
433 }
434
435 private class MimeBodyPartInputStream extends InputStream {
436 BufferedInputStream inStream;
437 public boolean boundaryFound = false;
438 byte[] boundary;
439 public boolean finalBoundaryFound = false;
440
441 public MimeBodyPartInputStream(BufferedInputStream inStream, byte[] boundary) {
442 super();
443 this.inStream = inStream;
444 this.boundary = boundary;
445 }
446
447 /**
448 * The base reading method for reading one character
449 * at a time.
450 *
451 * @return The read character, or -1 if an EOF was encountered.
452 * @exception IOException
453 */
454 public int read() throws IOException {
455 if (boundaryFound) {
456 return -1;
457 }
458
459 // read the next value from stream
460 int firstChar = inStream.read();
461 // premature end? Handle it like a boundary located
462 if (firstChar == -1) {
463 boundaryFound = true;
464 // also mark this as the end
465 finalBoundaryFound = true;
466 return -1;
467 }
468
469 // we first need to look for a line boundary. If we find a boundary, it can be followed by the
470 // boundary marker, so we need to remember what sort of thing we found, then read ahead looking
471 // for the part boundary.
472
473 // NB:, we only handle [\r]\n--boundary marker[--]
474 // we need to at least accept what most mail servers would consider an
475 // invalid format using just '\n'
476 if (firstChar != '\r' && firstChar != '\n') {
477 // not a \r, just return the byte as is
478 return firstChar;
479 }
480 // we might need to rewind to this point. The padding is to allow for
481 // line terminators and linear whitespace on the boundary lines
482 inStream.mark(boundary.length + 1000);
483 // we need to keep track of the first read character in case we need to
484 // rewind back to the mark point
485 int value = firstChar;
486 // if this is a '\r', then we require the '\n'
487 if (value == '\r') {
488 // now scan ahead for the second character
489 value = inStream.read();
490 if (value != '\n') {
491 // only a \r, so this can't be a boundary. Return the
492 // \r as if it was data, after first resetting
493 inStream.reset();
494 return '\r';
495 }
496 }
497
498 value = inStream.read();
499 // if the next character is not a boundary start, we
500 // need to handle this as a normal line end
501 if ((byte) value != boundary[0]) {
502 // just reset and return the first character as data
503 inStream.reset();
504 return firstChar;
505 }
506
507 // we're here because we found a "\r\n-" sequence, which is a potential
508 // boundary marker. Read the individual characters of the next line until
509 // we have a mismatch
510
511 // read value is the first byte of the boundary. Start matching the
512 // next characters to find a boundary
513 int boundaryIndex = 0;
514 while ((boundaryIndex < boundary.length) && ((byte) value == boundary[boundaryIndex])) {
515 value = inStream.read();
516 boundaryIndex++;
517 }
518 // if we didn't match all the way, we need to push back what we've read and
519 // return the EOL character
520 if (boundaryIndex != boundary.length) {
521 // Boundary not found. Restoring bytes skipped.
522 // just reset and return the first character as data
523 inStream.reset();
524 return firstChar;
525 }
526
527 // The full boundary sequence should be \r\n--boundary string[--]\r\n
528 // if the last character we read was a '-', check for the end terminator
529 if (value == '-') {
530 value = inStream.read();
531 // crud, we have a bad boundary terminator. We need to unwind this all the way
532 // back to the lineend and pretend none of this ever happened
533 if (value != '-') {
534 // Boundary not found. Restoring bytes skipped.
535 // just reset and return the first character as data
536 inStream.reset();
537 return firstChar;
538 }
539 // on the home stretch, but we need to verify the LWSP/EOL sequence
540 value = inStream.read();
541 // first skip over the linear whitespace
542 while (value == ' ' || value == '\t') {
543 value = inStream.read();
544 }
545
546 // We've matched the final boundary, skipped any whitespace, but
547 // we've hit the end of the stream. This is highly likely when
548 // we have nested multiparts, since the linend terminator for the
549 // final boundary marker is eated up as the start of the outer
550 // boundary marker. No CRLF sequence here is ok.
551 if (value == -1) {
552 // we've hit the end of times...
553 finalBoundaryFound = true;
554 // we have a boundary, so return this as an EOF condition
555 boundaryFound = true;
556 return -1;
557 }
558
559 // this must be a CR or a LF...which leaves us even more to push back and forget
560 if (value != '\r' && value != '\n') {
561 // Boundary not found. Restoring bytes skipped.
562 // just reset and return the first character as data
563 inStream.reset();
564 return firstChar;
565 }
566
567 // if this is carriage return, check for a linefeed
568 if (value == '\r') {
569 // last check, this must be a line feed
570 value = inStream.read();
571 if (value != '\n') {
572 // SO CLOSE!
573 // Boundary not found. Restoring bytes skipped.
574 // just reset and return the first character as data
575 inStream.reset();
576 return firstChar;
577 }
578 }
579
580 // we've hit the end of times...
581 finalBoundaryFound = true;
582 }
583 else {
584 // first skip over the linear whitespace
585 while (value == ' ' || value == '\t') {
586 value = inStream.read();
587 }
588 // this must be a CR or a LF...which leaves us even more to push back and forget
589 if (value != '\r' && value != '\n') {
590 // Boundary not found. Restoring bytes skipped.
591 // just reset and return the first character as data
592 inStream.reset();
593 return firstChar;
594 }
595
596 // if this is carriage return, check for a linefeed
597 if (value == '\r') {
598 // last check, this must be a line feed
599 value = inStream.read();
600 if (value != '\n') {
601 // SO CLOSE!
602 // Boundary not found. Restoring bytes skipped.
603 // just reset and return the first character as data
604 inStream.reset();
605 return firstChar;
606 }
607 }
608 }
609 // we have a boundary, so return this as an EOF condition
610 boundaryFound = true;
611 return -1;
612 }
613 }
614
615
616 /**
617 * Return true if the final boundary line for this multipart was
618 * seen when parsing the data.
619 *
620 * @return
621 * @exception MessagingException
622 */
623 public boolean isComplete() throws MessagingException {
624 // make sure we've parsed this
625 parse();
626 return complete;
627 }
628
629
630 /**
631 * Returns the preamble text that appears before the first bady
632 * part of a MIME multi part. The preamble is optional, so this
633 * might be null.
634 *
635 * @return The preamble text string.
636 * @exception MessagingException
637 */
638 public String getPreamble() throws MessagingException {
639 parse();
640 return preamble;
641 }
642
643 /**
644 * Set the message preamble text. This will be written before
645 * the first boundary of a multi-part message.
646 *
647 * @param preamble The new boundary text. This is complete lines of text, including
648 * new lines.
649 *
650 * @exception MessagingException
651 */
652 public void setPreamble(String preamble) throws MessagingException {
653 this.preamble = preamble;
654 }
655 }