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  import java.io.PushbackInputStream;
29  
30  import javax.activation.DataSource;
31  import javax.mail.BodyPart;
32  import javax.mail.MessagingException;
33  import javax.mail.Multipart;
34  import javax.mail.MultipartDataSource;
35  
36  import org.apache.geronimo.mail.util.SessionUtil;
37  
38  /**
39   * @version $Rev: 467553 $ $Date: 2006-10-25 00:01:51 -0400 (Wed, 25 Oct 2006) $
40   */
41  public class MimeMultipart extends Multipart {
42  	private static final String MIME_IGNORE_MISSING_BOUNDARY = "mail.mime.multipart.ignoremissingendboundary";
43  
44      /**
45       * DataSource that provides our InputStream.
46       */
47      protected DataSource ds;
48      /**
49       * Indicates if the data has been parsed.
50       */
51      protected boolean parsed = true;
52  
53      // the content type information
54      private transient ContentType type;
55  
56      // indicates if we've seen the final boundary line when parsing.
57      private boolean complete = true;
58  
59      // MIME multipart preable text that can appear before the first boundary line.
60      private String preamble = null;
61  
62      /**
63       * Create an empty MimeMultipart with content type "multipart/mixed"
64       */
65      public MimeMultipart() {
66          this("mixed");
67      }
68  
69      /**
70       * Create an empty MimeMultipart with the subtype supplied.
71       *
72       * @param subtype the subtype
73       */
74      public MimeMultipart(String subtype) {
75          type = new ContentType("multipart", subtype, null);
76          type.setParameter("boundary", getBoundary());
77          contentType = type.toString();
78      }
79  
80      /**
81       * Create a MimeMultipart from the supplied DataSource.
82       *
83       * @param dataSource the DataSource to use
84       * @throws MessagingException
85       */
86      public MimeMultipart(DataSource dataSource) throws MessagingException {
87          ds = dataSource;
88          if (dataSource instanceof MultipartDataSource) {
89              super.setMultipartDataSource((MultipartDataSource) dataSource);
90              parsed = true;
91          } else {
92              type = new ContentType(ds.getContentType());
93              contentType = type.toString();
94              parsed = false;
95          }
96      }
97  
98      public void setSubType(String subtype) throws MessagingException {
99          type.setSubType(subtype);
100         contentType = type.toString();
101     }
102 
103     public int getCount() throws MessagingException {
104         parse();
105         return super.getCount();
106     }
107 
108     public synchronized BodyPart getBodyPart(int part) throws MessagingException {
109         parse();
110         return super.getBodyPart(part);
111     }
112 
113     public BodyPart getBodyPart(String cid) throws MessagingException {
114         parse();
115         for (int i = 0; i < parts.size(); i++) {
116             MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
117             if (cid.equals(bodyPart.getContentID())) {
118                 return bodyPart;
119             }
120         }
121         return null;
122     }
123 
124     protected void updateHeaders() throws MessagingException {
125         parse();
126         for (int i = 0; i < parts.size(); i++) {
127             MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
128             bodyPart.updateHeaders();
129         }
130     }
131 
132     private static byte[] dash = { '-', '-' };
133     private static byte[] crlf = { 13, 10 };
134 
135     public void writeTo(OutputStream out) throws IOException, MessagingException {
136         parse();
137         String boundary = type.getParameter("boundary");
138         byte[] bytes = boundary.getBytes();
139 
140         if (preamble != null) {
141             byte[] preambleBytes = preamble.getBytes();
142             // write this out, followed by a line break.
143             out.write(preambleBytes);
144             out.write(crlf);
145         }
146 
147         for (int i = 0; i < parts.size(); i++) {
148             BodyPart bodyPart = (BodyPart) parts.get(i);
149             out.write(dash);
150             out.write(bytes);
151             out.write(crlf);
152             bodyPart.writeTo(out);
153             out.write(crlf);
154         }
155         out.write(dash);
156         out.write(bytes);
157         out.write(dash);
158         out.write(crlf);
159         out.flush();
160     }
161 
162     protected void parse() throws MessagingException {
163         if (parsed) {
164             return;
165         }
166         try {
167             ContentType cType = new ContentType(contentType);
168             byte[] boundary = ("--" + cType.getParameter("boundary")).getBytes();
169             InputStream is = new BufferedInputStream(ds.getInputStream());
170             PushbackInputStream pushbackInStream = new PushbackInputStream(is,
171                     (boundary.length + 2));
172             readTillFirstBoundary(pushbackInStream, boundary);
173             while (pushbackInStream.available()>0){
174                 MimeBodyPartInputStream partStream;
175                 partStream = new MimeBodyPartInputStream(pushbackInStream,
176                         boundary);
177                 addBodyPart(new MimeBodyPart(partStream));
178 
179                 // terminated by an EOF rather than a proper boundary?
180                 if (!partStream.boundaryFound) {
181                     if (!SessionUtil.getBooleanProperty(MIME_IGNORE_MISSING_BOUNDARY, true)) {
182                         throw new MessagingException("Missing Multi-part end boundary");
183                     }
184                     complete = false;
185                 }
186             }
187         } catch (Exception e){
188             throw new MessagingException(e.toString(),e);
189         }
190         parsed = true;
191     }
192 
193     /**
194      * Move the read pointer to the begining of the first part
195      * read till the end of first boundary.  Any data read before this point are
196      * saved as the preamble.
197      *
198      * @param pushbackInStream
199      * @param boundary
200      * @throws MessagingException
201      */
202     private boolean readTillFirstBoundary(PushbackInputStream pushbackInStream, byte[] boundary) throws MessagingException {
203         ByteArrayOutputStream preambleStream = new ByteArrayOutputStream();
204 
205         try {
206             while (pushbackInStream.available() > 0) {
207                 int value = pushbackInStream.read();
208                 if ((byte) value == boundary[0]) {
209                     int boundaryIndex = 0;
210                     while (pushbackInStream.available() > 0 && (boundaryIndex < boundary.length)
211                             && ((byte) value == boundary[boundaryIndex])) {
212                         value = pushbackInStream.read();
213                         if (value == -1)
214                             throw new MessagingException(
215                                     "Unexpected End of Stream while searching for first Mime Boundary");
216                         boundaryIndex++;
217                     }
218                     if (boundaryIndex == boundary.length) { // boundary found
219                         pushbackInStream.read();
220 
221                         // save the preamble, if there is one.
222                         byte[] preambleBytes = preambleStream.toByteArray();
223                         if (preambleBytes.length > 0) {
224                             preamble = new String(preambleBytes);
225                         }
226                         return true;
227                     }
228                     else {
229                         // we need to add this to the preamble.  We write the part of the boundary that
230                         // actually matched, followed by the character that was the mismatch.
231                         preambleStream.write(boundary, 0, boundaryIndex);
232                         preambleStream.write((byte)value);
233                     }
234                 }
235                 else {
236                     // this is part of the preamble.
237                     preambleStream.write((byte)value);
238                 }
239             }
240         } catch (IOException ioe) {
241             throw new MessagingException(ioe.toString(), ioe);
242         }
243         return false;
244     }
245 
246     protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException {
247         return new InternetHeaders(in);
248     }
249 
250     protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, byte[] data) throws MessagingException {
251         return new MimeBodyPart(headers, data);
252     }
253 
254     protected MimeBodyPart createMimeBodyPart(InputStream in) throws MessagingException {
255         return new MimeBodyPart(in);
256     }
257 
258     // static used to track boudary value allocations to help ensure uniqueness.
259     private static int part;
260 
261     private synchronized static String getBoundary() {
262         int i;
263         synchronized(MimeMultipart.class) {
264             i = part++;
265         }
266         StringBuffer buf = new StringBuffer(64);
267         buf.append("----=_Part_").append(i).append('_').append((new Object()).hashCode()).append('.').append(System.currentTimeMillis());
268         return buf.toString();
269     }
270 
271     private class MimeBodyPartInputStream extends InputStream {
272         PushbackInputStream inStream;
273         public boolean boundaryFound = false;
274         byte[] boundary;
275 
276         public MimeBodyPartInputStream(PushbackInputStream inStream,
277                                        byte[] boundary) {
278             super();
279             this.inStream = inStream;
280             this.boundary = boundary;
281         }
282 
283         public int read() throws IOException {
284             if (boundaryFound) {
285                 return -1;
286             }
287             // read the next value from stream
288             int value = inStream.read();
289             // 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.
290             // This logic introduced to handle it
291             //TODO look more in to this && for a better way to do this
292             if (value == 13) {
293                 value = inStream.read();
294                 if (value != 10) {
295                     inStream.unread(value);
296                     return 13;
297                 } else {
298                     value = inStream.read();
299                     if ((byte) value != boundary[0]) {
300                         inStream.unread(value);
301                         inStream.unread(10);
302                         return 13;
303                     }
304                 }
305             } else if ((byte) value != boundary[0]) {
306                 return value;
307             }
308             // read value is the first byte of the boundary. Start matching the
309             // next characters to find a boundary
310             int boundaryIndex = 0;
311             while ((boundaryIndex < boundary.length)
312                     && ((byte) value == boundary[boundaryIndex])) {
313                 value = inStream.read();
314                 boundaryIndex++;
315             }
316             if (boundaryIndex == boundary.length) { // boundary found
317                 boundaryFound = true;
318                 // read the end of line character
319                 if (inStream.read() == '-' && value == '-') {
320                     //Last mime boundary should have a succeeding "--"
321                     //as we are on it, read the terminating CRLF
322                     inStream.read();
323                     inStream.read();
324                 }
325                 return -1;
326             }
327             // Boundary not found. Restoring bytes skipped.
328             // write first skipped byte, push back the rest
329             if (value != -1) { // Stream might have ended
330                 inStream.unread(value);
331             }
332             inStream.unread(boundary, 1, boundaryIndex - 1);
333             return boundary[0];
334         }
335     }
336 
337     /**
338      * Return true if the final boundary line for this multipart was
339      * seen when parsing the data.
340      *
341      * @return
342      * @exception MessagingException
343      */
344     public boolean isComplete() throws MessagingException {
345         // make sure we've parsed this
346         parse();
347         return complete;
348     }
349 
350 
351     /**
352      * Returns the preamble text that appears before the first bady
353      * part of a MIME multi part.  The preamble is optional, so this
354      * might be null.
355      *
356      * @return The preamble text string.
357      * @exception MessagingException
358      */
359     public String getPreamble() throws MessagingException {
360         parse();
361         return preamble;
362     }
363 
364     /**
365      * Set the message preamble text.  This will be written before
366      * the first boundary of a multi-part message.
367      *
368      * @param preamble The new boundary text.  This is complete lines of text, including
369      *                 new lines.
370      *
371      * @exception MessagingException
372      */
373     public void setPreamble(String preamble) throws MessagingException {
374         this.preamble = preamble;
375     }
376 }