View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package javax.mail.internet;
21  
22  import java.io.BufferedInputStream;
23  import java.io.ByteArrayInputStream;
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  
29  import java.util.Arrays; 
30  
31  import javax.activation.DataSource;
32  import javax.mail.BodyPart;
33  import javax.mail.MessagingException;
34  import javax.mail.Multipart;
35  import javax.mail.MultipartDataSource;
36  
37  import org.apache.geronimo.mail.util.SessionUtil;
38  
39  /**
40   * @version $Rev: 689486 $ $Date: 2008-08-27 10:11:03 -0400 (Wed, 27 Aug 2008) $
41   */
42  public class MimeMultipart extends Multipart {
43  	private static final String MIME_IGNORE_MISSING_BOUNDARY = "mail.mime.multipart.ignoremissingendboundary";
44  
45      /**
46       * DataSource that provides our InputStream.
47       */
48      protected DataSource ds;
49      /**
50       * Indicates if the data has been parsed.
51       */
52      protected boolean parsed = true;
53  
54      // the content type information
55      private transient ContentType type;
56  
57      // indicates if we've seen the final boundary line when parsing.
58      private boolean complete = true;
59  
60      // MIME multipart preable text that can appear before the first boundary line.
61      private String preamble = null;
62  
63      /**
64       * Create an empty MimeMultipart with content type "multipart/mixed"
65       */
66      public MimeMultipart() {
67          this("mixed");
68      }
69  
70      /**
71       * Create an empty MimeMultipart with the subtype supplied.
72       *
73       * @param subtype the subtype
74       */
75      public MimeMultipart(String subtype) {
76          type = new ContentType("multipart", subtype, null);
77          type.setParameter("boundary", getBoundary());
78          contentType = type.toString();
79      }
80  
81      /**
82       * Create a MimeMultipart from the supplied DataSource.
83       *
84       * @param dataSource the DataSource to use
85       * @throws MessagingException
86       */
87      public MimeMultipart(DataSource dataSource) throws MessagingException {
88          ds = dataSource;
89          if (dataSource instanceof MultipartDataSource) {
90              super.setMultipartDataSource((MultipartDataSource) dataSource);
91              parsed = true;
92          } else {
93              // We keep the original, provided content type string so that we 
94              // don't end up changing quoting/formatting of the header unless 
95              // changes are made to the content type.  James is somewhat dependent 
96              // on that behavior. 
97              contentType = ds.getContentType(); 
98              type = new ContentType(contentType);
99              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 }