001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package javax.mail.internet;
021    
022    import java.io.BufferedInputStream;
023    import java.io.ByteArrayInputStream;
024    import java.io.ByteArrayOutputStream;
025    import java.io.IOException;
026    import java.io.InputStream;
027    import java.io.OutputStream;
028    import java.io.PushbackInputStream;
029    
030    import javax.activation.DataSource;
031    import javax.mail.BodyPart;
032    import javax.mail.MessagingException;
033    import javax.mail.Multipart;
034    import javax.mail.MultipartDataSource;
035    
036    import org.apache.geronimo.mail.util.SessionUtil;
037    
038    /**
039     * @version $Rev: 467553 $ $Date: 2006-10-25 00:01:51 -0400 (Wed, 25 Oct 2006) $
040     */
041    public class MimeMultipart extends Multipart {
042            private static final String MIME_IGNORE_MISSING_BOUNDARY = "mail.mime.multipart.ignoremissingendboundary";
043    
044        /**
045         * DataSource that provides our InputStream.
046         */
047        protected DataSource ds;
048        /**
049         * Indicates if the data has been parsed.
050         */
051        protected boolean parsed = true;
052    
053        // the content type information
054        private transient ContentType type;
055    
056        // indicates if we've seen the final boundary line when parsing.
057        private boolean complete = true;
058    
059        // MIME multipart preable text that can appear before the first boundary line.
060        private String preamble = null;
061    
062        /**
063         * Create an empty MimeMultipart with content type "multipart/mixed"
064         */
065        public MimeMultipart() {
066            this("mixed");
067        }
068    
069        /**
070         * Create an empty MimeMultipart with the subtype supplied.
071         *
072         * @param subtype the subtype
073         */
074        public MimeMultipart(String subtype) {
075            type = new ContentType("multipart", subtype, null);
076            type.setParameter("boundary", getBoundary());
077            contentType = type.toString();
078        }
079    
080        /**
081         * Create a MimeMultipart from the supplied DataSource.
082         *
083         * @param dataSource the DataSource to use
084         * @throws MessagingException
085         */
086        public MimeMultipart(DataSource dataSource) throws MessagingException {
087            ds = dataSource;
088            if (dataSource instanceof MultipartDataSource) {
089                super.setMultipartDataSource((MultipartDataSource) dataSource);
090                parsed = true;
091            } else {
092                type = new ContentType(ds.getContentType());
093                contentType = type.toString();
094                parsed = false;
095            }
096        }
097    
098        public void setSubType(String subtype) throws MessagingException {
099            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    }