001 /** 002 * 003 * Copyright 2003-2006 The Apache Software Foundation 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package javax.mail.internet; 019 020 import java.io.BufferedInputStream; 021 import java.io.ByteArrayInputStream; 022 import java.io.ByteArrayOutputStream; 023 import java.io.IOException; 024 import java.io.InputStream; 025 import java.io.OutputStream; 026 import java.io.PushbackInputStream; 027 028 import javax.activation.DataSource; 029 import javax.mail.BodyPart; 030 import javax.mail.MessagingException; 031 import javax.mail.Multipart; 032 import javax.mail.MultipartDataSource; 033 034 import org.apache.geronimo.mail.util.SessionUtil; 035 036 /** 037 * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $ 038 */ 039 public class MimeMultipart extends Multipart { 040 private static final String MIME_IGNORE_MISSING_BOUNDARY = "mail.mime.multipart.ignoremissingendboundary"; 041 042 /** 043 * DataSource that provides our InputStream. 044 */ 045 protected DataSource ds; 046 /** 047 * Indicates if the data has been parsed. 048 */ 049 protected boolean parsed = true; 050 051 // the content type information 052 private transient ContentType type; 053 054 // indicates if we've seen the final boundary line when parsing. 055 private boolean complete = true; 056 057 // MIME multipart preable text that can appear before the first boundary line. 058 private String preamble = null; 059 060 /** 061 * Create an empty MimeMultipart with content type "multipart/mixed" 062 */ 063 public MimeMultipart() { 064 this("mixed"); 065 } 066 067 /** 068 * Create an empty MimeMultipart with the subtype supplied. 069 * 070 * @param subtype the subtype 071 */ 072 public MimeMultipart(String subtype) { 073 type = new ContentType("multipart", subtype, null); 074 type.setParameter("boundary", getBoundary()); 075 contentType = type.toString(); 076 } 077 078 /** 079 * Create a MimeMultipart from the supplied DataSource. 080 * 081 * @param dataSource the DataSource to use 082 * @throws MessagingException 083 */ 084 public MimeMultipart(DataSource dataSource) throws MessagingException { 085 ds = dataSource; 086 if (dataSource instanceof MultipartDataSource) { 087 super.setMultipartDataSource((MultipartDataSource) dataSource); 088 parsed = true; 089 } else { 090 type = new ContentType(ds.getContentType()); 091 contentType = type.toString(); 092 parsed = false; 093 } 094 } 095 096 public void setSubType(String subtype) throws MessagingException { 097 type.setSubType(subtype); 098 contentType = type.toString(); 099 } 100 101 public int getCount() throws MessagingException { 102 parse(); 103 return super.getCount(); 104 } 105 106 public synchronized BodyPart getBodyPart(int part) throws MessagingException { 107 parse(); 108 return super.getBodyPart(part); 109 } 110 111 public BodyPart getBodyPart(String cid) throws MessagingException { 112 parse(); 113 for (int i = 0; i < parts.size(); i++) { 114 MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i); 115 if (cid.equals(bodyPart.getContentID())) { 116 return bodyPart; 117 } 118 } 119 return null; 120 } 121 122 protected void updateHeaders() throws MessagingException { 123 parse(); 124 for (int i = 0; i < parts.size(); i++) { 125 MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i); 126 bodyPart.updateHeaders(); 127 } 128 } 129 130 private static byte[] dash = { '-', '-' }; 131 private static byte[] crlf = { 13, 10 }; 132 133 public void writeTo(OutputStream out) throws IOException, MessagingException { 134 parse(); 135 String boundary = type.getParameter("boundary"); 136 byte[] bytes = boundary.getBytes(); 137 138 if (preamble != null) { 139 byte[] preambleBytes = preamble.getBytes(); 140 // write this out, followed by a line break. 141 out.write(preambleBytes); 142 out.write(crlf); 143 } 144 145 for (int i = 0; i < parts.size(); i++) { 146 BodyPart bodyPart = (BodyPart) parts.get(i); 147 out.write(dash); 148 out.write(bytes); 149 out.write(crlf); 150 bodyPart.writeTo(out); 151 out.write(crlf); 152 } 153 out.write(dash); 154 out.write(bytes); 155 out.write(dash); 156 out.write(crlf); 157 out.flush(); 158 } 159 160 protected void parse() throws MessagingException { 161 if (parsed) { 162 return; 163 } 164 try { 165 ContentType cType = new ContentType(contentType); 166 byte[] boundary = ("--" + cType.getParameter("boundary")).getBytes(); 167 InputStream is = new BufferedInputStream(ds.getInputStream()); 168 PushbackInputStream pushbackInStream = new PushbackInputStream(is, 169 (boundary.length + 2)); 170 readTillFirstBoundary(pushbackInStream, boundary); 171 while (pushbackInStream.available()>0){ 172 MimeBodyPartInputStream partStream; 173 partStream = new MimeBodyPartInputStream(pushbackInStream, 174 boundary); 175 addBodyPart(new MimeBodyPart(partStream)); 176 177 // terminated by an EOF rather than a proper boundary? 178 if (!partStream.boundaryFound) { 179 if (!SessionUtil.getBooleanProperty(MIME_IGNORE_MISSING_BOUNDARY, true)) { 180 throw new MessagingException("Missing Multi-part end boundary"); 181 } 182 complete = false; 183 } 184 } 185 } catch (Exception e){ 186 throw new MessagingException(e.toString(),e); 187 } 188 parsed = true; 189 } 190 191 /** 192 * Move the read pointer to the begining of the first part 193 * read till the end of first boundary. Any data read before this point are 194 * saved as the preamble. 195 * 196 * @param pushbackInStream 197 * @param boundary 198 * @throws MessagingException 199 */ 200 private boolean readTillFirstBoundary(PushbackInputStream pushbackInStream, byte[] boundary) throws MessagingException { 201 ByteArrayOutputStream preambleStream = new ByteArrayOutputStream(); 202 203 try { 204 while (pushbackInStream.available() > 0) { 205 int value = pushbackInStream.read(); 206 if ((byte) value == boundary[0]) { 207 int boundaryIndex = 0; 208 while (pushbackInStream.available() > 0 && (boundaryIndex < boundary.length) 209 && ((byte) value == boundary[boundaryIndex])) { 210 value = pushbackInStream.read(); 211 if (value == -1) 212 throw new MessagingException( 213 "Unexpected End of Stream while searching for first Mime Boundary"); 214 boundaryIndex++; 215 } 216 if (boundaryIndex == boundary.length) { // boundary found 217 pushbackInStream.read(); 218 219 // save the preamble, if there is one. 220 byte[] preambleBytes = preambleStream.toByteArray(); 221 if (preambleBytes.length > 0) { 222 preamble = new String(preambleBytes); 223 } 224 return true; 225 } 226 else { 227 // we need to add this to the preamble. We write the part of the boundary that 228 // actually matched, followed by the character that was the mismatch. 229 preambleStream.write(boundary, 0, boundaryIndex); 230 preambleStream.write((byte)value); 231 } 232 } 233 else { 234 // this is part of the preamble. 235 preambleStream.write((byte)value); 236 } 237 } 238 } catch (IOException ioe) { 239 throw new MessagingException(ioe.toString(), ioe); 240 } 241 return false; 242 } 243 244 protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException { 245 return new InternetHeaders(in); 246 } 247 248 protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, byte[] data) throws MessagingException { 249 return new MimeBodyPart(headers, data); 250 } 251 252 protected MimeBodyPart createMimeBodyPart(InputStream in) throws MessagingException { 253 return new MimeBodyPart(in); 254 } 255 256 // static used to track boudary value allocations to help ensure uniqueness. 257 private static int part; 258 259 private synchronized static String getBoundary() { 260 int i; 261 synchronized(MimeMultipart.class) { 262 i = part++; 263 } 264 StringBuffer buf = new StringBuffer(64); 265 buf.append("----=_Part_").append(i).append('_').append((new Object()).hashCode()).append('.').append(System.currentTimeMillis()); 266 return buf.toString(); 267 } 268 269 private class MimeBodyPartInputStream extends InputStream { 270 PushbackInputStream inStream; 271 public boolean boundaryFound = false; 272 byte[] boundary; 273 274 public MimeBodyPartInputStream(PushbackInputStream inStream, 275 byte[] boundary) { 276 super(); 277 this.inStream = inStream; 278 this.boundary = boundary; 279 } 280 281 public int read() throws IOException { 282 if (boundaryFound) { 283 return -1; 284 } 285 // read the next value from stream 286 int value = inStream.read(); 287 // A problem occured because all the mime parts tends to have a /r/n at the end. Making it hard to transform them to correct DataSources. 288 // This logic introduced to handle it 289 //TODO look more in to this && for a better way to do this 290 if (value == 13) { 291 value = inStream.read(); 292 if (value != 10) { 293 inStream.unread(value); 294 return 13; 295 } else { 296 value = inStream.read(); 297 if ((byte) value != boundary[0]) { 298 inStream.unread(value); 299 inStream.unread(10); 300 return 13; 301 } 302 } 303 } else if ((byte) value != boundary[0]) { 304 return value; 305 } 306 // read value is the first byte of the boundary. Start matching the 307 // next characters to find a boundary 308 int boundaryIndex = 0; 309 while ((boundaryIndex < boundary.length) 310 && ((byte) value == boundary[boundaryIndex])) { 311 value = inStream.read(); 312 boundaryIndex++; 313 } 314 if (boundaryIndex == boundary.length) { // boundary found 315 boundaryFound = true; 316 // read the end of line character 317 if (inStream.read() == '-' && value == '-') { 318 //Last mime boundary should have a succeeding "--" 319 //as we are on it, read the terminating CRLF 320 inStream.read(); 321 inStream.read(); 322 } 323 return -1; 324 } 325 // Boundary not found. Restoring bytes skipped. 326 // write first skipped byte, push back the rest 327 if (value != -1) { // Stream might have ended 328 inStream.unread(value); 329 } 330 inStream.unread(boundary, 1, boundaryIndex - 1); 331 return boundary[0]; 332 } 333 } 334 335 /** 336 * Return true if the final boundary line for this multipart was 337 * seen when parsing the data. 338 * 339 * @return 340 * @exception MessagingException 341 */ 342 public boolean isComplete() throws MessagingException { 343 // make sure we've parsed this 344 parse(); 345 return complete; 346 } 347 348 349 /** 350 * Returns the preamble text that appears before the first bady 351 * part of a MIME multi part. The preamble is optional, so this 352 * might be null. 353 * 354 * @return The preamble text string. 355 * @exception MessagingException 356 */ 357 public String getPreamble() throws MessagingException { 358 parse(); 359 return preamble; 360 } 361 362 /** 363 * Set the message preamble text. This will be written before 364 * the first boundary of a multi-part message. 365 * 366 * @param preamble The new boundary text. This is complete lines of text, including 367 * new lines. 368 * 369 * @exception MessagingException 370 */ 371 public void setPreamble(String preamble) throws MessagingException { 372 this.preamble = preamble; 373 } 374 }