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 }