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    }