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.IOException;
023    import java.io.InputStream;
024    import java.io.OutputStream;
025    import java.io.BufferedInputStream;
026    import java.io.PushbackInputStream;
027    import javax.activation.DataSource;
028    import javax.mail.BodyPart;
029    import javax.mail.MessagingException;
030    import javax.mail.Multipart;
031    import javax.mail.MultipartDataSource;
032    
033    /**
034     * @version $Rev: 467553 $ $Date: 2006-10-25 06:01:51 +0200 (Mi, 25. Okt 2006) $
035     */
036    public class MimeMultipart extends Multipart {
037        /**
038         * DataSource that provides our InputStream.
039         */
040        protected DataSource ds;
041        /**
042         * Indicates if the data has been parsed.
043         */
044        protected boolean parsed = true;
045    
046        private transient ContentType type;
047    
048        /**
049         * Create an empty MimeMultipart with content type "multipart/mixed"
050         */
051        public MimeMultipart() {
052            this("mixed");
053        }
054    
055        /**
056         * Create an empty MimeMultipart with the subtype supplied.
057         *
058         * @param subtype the subtype
059         */
060        public MimeMultipart(String subtype) {
061            type = new ContentType("multipart", subtype, null);
062            type.setParameter("boundary", getBoundary());
063            contentType = type.toString();
064        }
065    
066        /**
067         * Create a MimeMultipart from the supplied DataSource.
068         *
069         * @param dataSource the DataSource to use
070         * @throws MessagingException
071         */
072        public MimeMultipart(DataSource dataSource) throws MessagingException {
073            ds = dataSource;
074            if (dataSource instanceof MultipartDataSource) {
075                super.setMultipartDataSource((MultipartDataSource) dataSource);
076                parsed = true;
077            } else {
078                type = new ContentType(ds.getContentType());
079                contentType = type.toString();
080                parsed = false;
081            }
082        }
083    
084        public void setSubType(String subtype) throws MessagingException {
085            type.setSubType(subtype);
086            contentType = type.toString();
087        }
088    
089        public int getCount() throws MessagingException {
090            parse();
091            return super.getCount();
092        }
093    
094        public synchronized BodyPart getBodyPart(int part) throws MessagingException {
095            parse();
096            return super.getBodyPart(part);
097        }
098    
099        public BodyPart getBodyPart(String cid) throws MessagingException {
100            parse();
101            for (int i = 0; i < parts.size(); i++) {
102                MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
103                if (cid.equals(bodyPart.getContentID())) {
104                    return bodyPart;
105                }
106            }
107            return null;
108        }
109    
110        protected void updateHeaders() throws MessagingException {
111            parse();
112            for (int i = 0; i < parts.size(); i++) {
113                MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
114                bodyPart.updateHeaders();
115            }
116        }
117    
118        private static byte[] dash = { '-', '-' };
119        private static byte[] crlf = { 13, 10 };
120    
121        public void writeTo(OutputStream out) throws IOException, MessagingException {
122            parse();
123            String boundary = type.getParameter("boundary");
124            byte[] bytes = boundary.getBytes();
125            for (int i = 0; i < parts.size(); i++) {
126                BodyPart bodyPart = (BodyPart) parts.get(i);
127                out.write(dash);
128                out.write(bytes);
129                out.write(crlf);
130                bodyPart.writeTo(out);
131                out.write(crlf);
132            }
133            out.write(dash);
134            out.write(bytes);
135            out.write(dash);
136            out.write(crlf);
137            out.flush();
138        }
139    
140        protected void parse() throws MessagingException {
141            if (parsed) {
142                return;
143            }
144            try {
145                ContentType cType = new ContentType(contentType);
146                byte[] boundary = ("--" + cType.getParameter("boundary")).getBytes();
147                InputStream is = new BufferedInputStream(ds.getInputStream());
148                PushbackInputStream pushbackInStream = new PushbackInputStream(is,
149                        (boundary.length + 2));
150                readTillFirstBoundary(pushbackInStream, boundary);
151                while (pushbackInStream.available()>0){
152                    MimeBodyPartInputStream partStream;
153                    partStream = new MimeBodyPartInputStream(pushbackInStream,
154                            boundary);
155                    addBodyPart(new MimeBodyPart(partStream));
156                }
157            } catch (Exception e){
158                throw new MessagingException(e.toString(),e);
159            }
160            parsed = true;
161        }
162    
163        /**
164         * Move the read pointer to the begining of the first part
165         * read till the end of first boundary
166         *
167         * @param pushbackInStream
168         * @param boundary
169         * @throws MessagingException
170         */
171        private boolean readTillFirstBoundary(PushbackInputStream pushbackInStream, byte[] boundary) throws MessagingException {
172            try {
173                while (pushbackInStream.available() > 0) {
174                    int value = pushbackInStream.read();
175                    if ((byte) value == boundary[0]) {
176                        int boundaryIndex = 0;
177                        while (pushbackInStream.available() > 0 && (boundaryIndex < boundary.length)
178                                && ((byte) value == boundary[boundaryIndex])) {
179                            value = pushbackInStream.read();
180                            if (value == -1)
181                                throw new MessagingException(
182                                        "Unexpected End of Stream while searching for first Mime Boundary");
183                            boundaryIndex++;
184                        }
185                        if (boundaryIndex == boundary.length) { // boundary found
186                            pushbackInStream.read();
187                            return true;
188                        }
189                    }
190                }
191            } catch (IOException ioe) {
192                throw new MessagingException(ioe.toString(), ioe);
193            }
194            return false;
195        }
196    
197        protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException {
198            return new InternetHeaders(in);
199        }
200    
201        protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, byte[] data) throws MessagingException {
202            return new MimeBodyPart(headers, data);
203        }
204    
205        protected MimeBodyPart createMimeBodyPart(InputStream in) throws MessagingException {
206            return new MimeBodyPart(in);
207        }
208    
209        private static int part;
210    
211        private synchronized static String getBoundary() {
212            int i;
213            synchronized(MimeMultipart.class) {
214                i = part++;
215            }
216            StringBuffer buf = new StringBuffer(64);
217            buf.append("----=_Part_").append(i).append('_').append((new Object()).hashCode()).append('.').append(System.currentTimeMillis());
218            return buf.toString();
219        }
220    
221        private class MimeBodyPartInputStream extends InputStream {
222            PushbackInputStream inStream;
223            boolean boundaryFound = false;
224            byte[] boundary;
225    
226            public MimeBodyPartInputStream(PushbackInputStream inStream,
227                                           byte[] boundary) {
228                super();
229                this.inStream = inStream;
230                this.boundary = boundary;
231            }
232    
233            public int read() throws IOException {
234                if (boundaryFound) {
235                    return -1;
236                }
237                // read the next value from stream
238                int value = inStream.read();
239                // 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.
240                // This logic introduced to handle it
241                //TODO look more in to this && for a better way to do this
242                if (value == 13) {
243                    value = inStream.read();
244                    if (value != 10) {
245                        inStream.unread(value);
246                        return 13;
247                    } else {
248                        value = inStream.read();
249                        if ((byte) value != boundary[0]) {
250                            inStream.unread(value);
251                            inStream.unread(10);
252                            return 13;
253                        }
254                    }
255                } else if ((byte) value != boundary[0]) {
256                    return value;
257                }
258                // read value is the first byte of the boundary. Start matching the
259                // next characters to find a boundary
260                int boundaryIndex = 0;
261                while ((boundaryIndex < boundary.length)
262                        && ((byte) value == boundary[boundaryIndex])) {
263                    value = inStream.read();
264                    boundaryIndex++;
265                }
266                if (boundaryIndex == boundary.length) { // boundary found
267                    boundaryFound = true;
268                    // read the end of line character
269                    if (inStream.read() == 45 && value == 45) {
270                        //Last mime boundary should have a succeeding "--"
271                        //as we are on it, read the terminating CRLF
272                        inStream.read();
273                        inStream.read();
274                    }
275                    return -1;
276                }
277                // Boundary not found. Restoring bytes skipped.
278                // write first skipped byte, push back the rest
279                if (value != -1) { // Stream might have ended
280                    inStream.unread(value);
281                }
282                inStream.unread(boundary, 1, boundaryIndex - 1);
283                return boundary[0];
284            }
285        }
286    }