View Javadoc

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