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    
029    import java.util.Arrays; 
030    
031    import javax.activation.DataSource;
032    import javax.mail.BodyPart;
033    import javax.mail.MessagingException;
034    import javax.mail.Multipart;
035    import javax.mail.MultipartDataSource;
036    
037    import org.apache.geronimo.mail.util.SessionUtil;
038    
039    /**
040     * @version $Rev: 689486 $ $Date: 2008-08-27 10:11:03 -0400 (Wed, 27 Aug 2008) $
041     */
042    public class MimeMultipart extends Multipart {
043            private static final String MIME_IGNORE_MISSING_BOUNDARY = "mail.mime.multipart.ignoremissingendboundary";
044    
045        /**
046         * DataSource that provides our InputStream.
047         */
048        protected DataSource ds;
049        /**
050         * Indicates if the data has been parsed.
051         */
052        protected boolean parsed = true;
053    
054        // the content type information
055        private transient ContentType type;
056    
057        // indicates if we've seen the final boundary line when parsing.
058        private boolean complete = true;
059    
060        // MIME multipart preable text that can appear before the first boundary line.
061        private String preamble = null;
062    
063        /**
064         * Create an empty MimeMultipart with content type "multipart/mixed"
065         */
066        public MimeMultipart() {
067            this("mixed");
068        }
069    
070        /**
071         * Create an empty MimeMultipart with the subtype supplied.
072         *
073         * @param subtype the subtype
074         */
075        public MimeMultipart(String subtype) {
076            type = new ContentType("multipart", subtype, null);
077            type.setParameter("boundary", getBoundary());
078            contentType = type.toString();
079        }
080    
081        /**
082         * Create a MimeMultipart from the supplied DataSource.
083         *
084         * @param dataSource the DataSource to use
085         * @throws MessagingException
086         */
087        public MimeMultipart(DataSource dataSource) throws MessagingException {
088            ds = dataSource;
089            if (dataSource instanceof MultipartDataSource) {
090                super.setMultipartDataSource((MultipartDataSource) dataSource);
091                parsed = true;
092            } else {
093                // We keep the original, provided content type string so that we 
094                // don't end up changing quoting/formatting of the header unless 
095                // changes are made to the content type.  James is somewhat dependent 
096                // on that behavior. 
097                contentType = ds.getContentType(); 
098                type = new ContentType(contentType);
099                parsed = false;
100            }
101        }
102    
103        public void setSubType(String subtype) throws MessagingException {
104            type.setSubType(subtype);
105            contentType = type.toString();
106        }
107    
108        public int getCount() throws MessagingException {
109            parse();
110            return super.getCount();
111        }
112    
113        public synchronized BodyPart getBodyPart(int part) throws MessagingException {
114            parse();
115            return super.getBodyPart(part);
116        }
117    
118        public BodyPart getBodyPart(String cid) throws MessagingException {
119            parse();
120            for (int i = 0; i < parts.size(); i++) {
121                MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
122                if (cid.equals(bodyPart.getContentID())) {
123                    return bodyPart;
124                }
125            }
126            return null;
127        }
128    
129        protected void updateHeaders() throws MessagingException {
130            parse();
131            for (int i = 0; i < parts.size(); i++) {
132                MimeBodyPart bodyPart = (MimeBodyPart) parts.get(i);
133                bodyPart.updateHeaders();
134            }
135        }
136    
137        private static byte[] dash = { '-', '-' };
138        private static byte[] crlf = { 13, 10 };
139    
140        public void writeTo(OutputStream out) throws IOException, MessagingException {
141            parse();
142            String boundary = type.getParameter("boundary");
143            byte[] bytes = boundary.getBytes();
144    
145            if (preamble != null) {
146                byte[] preambleBytes = preamble.getBytes();
147                // write this out, followed by a line break.
148                out.write(preambleBytes);
149                out.write(crlf);
150            }
151    
152            for (int i = 0; i < parts.size(); i++) {
153                BodyPart bodyPart = (BodyPart) parts.get(i);
154                out.write(dash);
155                out.write(bytes);
156                out.write(crlf);
157                bodyPart.writeTo(out);
158                out.write(crlf);
159            }
160            out.write(dash);
161            out.write(bytes);
162            out.write(dash);
163            out.write(crlf);
164            out.flush();
165        }
166    
167        protected void parse() throws MessagingException {
168            if (parsed) {
169                return;
170            }
171            
172            try {
173                ContentType cType = new ContentType(contentType);
174                InputStream is = new BufferedInputStream(ds.getInputStream());
175                BufferedInputStream pushbackInStream = null;
176                String boundaryString = cType.getParameter("boundary"); 
177                byte[] boundary = null; 
178                if (boundaryString == null) {
179                    pushbackInStream = new BufferedInputStream(is, 1200);  
180                    // read until we find something that looks like a boundary string 
181                    boundary = readTillFirstBoundary(pushbackInStream); 
182                }
183                else {
184                    boundary = ("--" + boundaryString).getBytes();
185                    pushbackInStream = new BufferedInputStream(is, boundary.length + 1000);
186                    readTillFirstBoundary(pushbackInStream, boundary);
187                }
188                
189                while (true) {
190                    MimeBodyPartInputStream partStream;
191                    partStream = new MimeBodyPartInputStream(pushbackInStream, boundary);
192                    addBodyPart(new MimeBodyPart(partStream));
193    
194                    // terminated by an EOF rather than a proper boundary?
195                    if (!partStream.boundaryFound) {
196                        if (!SessionUtil.getBooleanProperty(MIME_IGNORE_MISSING_BOUNDARY, true)) {
197                            throw new MessagingException("Missing Multi-part end boundary");
198                        }
199                        complete = false;
200                    }
201                    // if we hit the final boundary, stop processing this 
202                    if (partStream.finalBoundaryFound) {
203                        break; 
204                    }
205                }
206            } catch (Exception e){
207                throw new MessagingException(e.toString(),e);
208            }
209            parsed = true;
210        }
211    
212        /**
213         * Move the read pointer to the begining of the first part
214         * read till the end of first boundary.  Any data read before this point are
215         * saved as the preamble.
216         *
217         * @param pushbackInStream
218         * @param boundary
219         * @throws MessagingException
220         */
221        private byte[] readTillFirstBoundary(BufferedInputStream pushbackInStream) throws MessagingException {
222            ByteArrayOutputStream preambleStream = new ByteArrayOutputStream();
223    
224            try {
225                while (true) {
226                    // read the next line 
227                    byte[] line = readLine(pushbackInStream); 
228                    // hit an EOF?
229                    if (line == null) {
230                        throw new MessagingException("Unexpected End of Stream while searching for first Mime Boundary");
231                    }
232                    // if this looks like a boundary, then make it so 
233                    if (line.length > 2 && line[0] == '-' && line[1] == '-') {
234                        // save the preamble, if there is one.
235                        byte[] preambleBytes = preambleStream.toByteArray();
236                        if (preambleBytes.length > 0) {
237                            preamble = new String(preambleBytes);
238                        }
239                        return stripLinearWhiteSpace(line);        
240                    }
241                    else {
242                        // this is part of the preamble.
243                        preambleStream.write(line);
244                        preambleStream.write('\r'); 
245                        preambleStream.write('\n'); 
246                    }
247                }
248            } catch (IOException ioe) {
249                throw new MessagingException(ioe.toString(), ioe);
250            }
251        }
252        
253        
254        /**
255         * Scan a line buffer stripping off linear whitespace 
256         * characters, returning a new array without the 
257         * characters, if possible. 
258         * 
259         * @param line   The source line buffer.
260         * 
261         * @return A byte array with white space characters removed, 
262         *         if necessary.
263         */
264        private byte[] stripLinearWhiteSpace(byte[] line) {
265            int index = line.length - 1; 
266            // if the last character is not a space or tab, we 
267            // can use this unchanged 
268            if (line[index] != ' ' && line[index] != '\t') {
269                return line; 
270            }
271            // scan backwards for the first non-white space 
272            for (; index > 0; index--) {
273                if (line[index] != ' ' && line[index] != '\t') {
274                    break;       
275                }
276            }
277            // make a shorter copy of this 
278            byte[] newLine = new byte[index + 1]; 
279            System.arraycopy(line, 0, newLine, 0, index + 1); 
280            return newLine; 
281        }
282    
283        /**
284         * Move the read pointer to the begining of the first part
285         * read till the end of first boundary.  Any data read before this point are
286         * saved as the preamble.
287         *
288         * @param pushbackInStream
289         * @param boundary
290         * @throws MessagingException
291         */
292        private void readTillFirstBoundary(BufferedInputStream pushbackInStream, byte[] boundary) throws MessagingException {
293            ByteArrayOutputStream preambleStream = new ByteArrayOutputStream();
294    
295            try {
296                while (true) {
297                    // read the next line 
298                    byte[] line = readLine(pushbackInStream); 
299                    // hit an EOF?
300                    if (line == null) {
301                        throw new MessagingException("Unexpected End of Stream while searching for first Mime Boundary");
302                    }
303                    
304                    // apply the boundary comparison rules to this 
305                    if (compareBoundary(line, boundary)) {
306                        // save the preamble, if there is one.
307                        byte[] preambleBytes = preambleStream.toByteArray();
308                        if (preambleBytes.length > 0) {
309                            preamble = new String(preambleBytes);
310                        }
311                        return;        
312                    }
313                    
314                    // this is part of the preamble.
315                    preambleStream.write(line);
316                    preambleStream.write('\r'); 
317                    preambleStream.write('\n'); 
318                }
319            } catch (IOException ioe) {
320                throw new MessagingException(ioe.toString(), ioe);
321            }
322        }
323        
324        
325        /**
326         * Peform a boundary comparison, taking into account 
327         * potential linear white space 
328         * 
329         * @param line     The line to compare.
330         * @param boundary The boundary we're searching for
331         * 
332         * @return true if this is a valid boundary line, false for 
333         *         any mismatches.
334         */
335        private boolean compareBoundary(byte[] line, byte[] boundary) {
336            // if the line is too short, this is an easy failure 
337            if (line.length < boundary.length) {
338                return false;
339            }
340            
341            // this is the most common situation
342            if (line.length == boundary.length) {
343                return Arrays.equals(line, boundary); 
344            }
345            // the line might have linear white space after the boundary portions
346            for (int i = 0; i < boundary.length; i++) {
347                // fail on any mismatch 
348                if (line[i] != boundary[i]) {
349                    return false; 
350                }
351            }
352            // everything after the boundary portion must be linear whitespace 
353            for (int i = boundary.length; i < line.length; i++) {
354                // fail on any mismatch 
355                if (line[i] != ' ' && line[i] != '\t') { 
356                    return false; 
357                }
358            }
359            // these are equivalent 
360            return true; 
361        }
362        
363        /**
364         * Read a single line of data from the input stream, 
365         * returning it as an array of bytes. 
366         * 
367         * @param in     The source input stream.
368         * 
369         * @return A byte array containing the line data.  Returns 
370         *         null if there's nothing left in the stream.
371         * @exception MessagingException
372         */
373        private byte[] readLine(BufferedInputStream in) throws IOException 
374        {
375            ByteArrayOutputStream line = new ByteArrayOutputStream();
376            
377            while (in.available() > 0) {
378                int value = in.read(); 
379                if (value == -1) {
380                    // if we have nothing in the accumulator, signal an EOF back 
381                    if (line.size() == 0) {
382                        return null; 
383                    }
384                    break; 
385                }
386                else if (value == '\r') {
387                    in.mark(10); 
388                    value = in.read(); 
389                    // we expect to find a linefeed after the carriage return, but 
390                    // some things play loose with the rules. 
391                    if (value != '\n') {
392                        in.reset(); 
393                    }
394                    break; 
395                }
396                else if (value == '\n') {
397                    // naked linefeed, allow that 
398                    break; 
399                }
400                else {
401                    // write this to the line 
402                    line.write((byte)value); 
403                }
404            }
405            // return this as an array of bytes 
406            return line.toByteArray(); 
407        }
408        
409    
410        protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException {
411            return new InternetHeaders(in);
412        }
413    
414        protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, byte[] data) throws MessagingException {
415            return new MimeBodyPart(headers, data);
416        }
417    
418        protected MimeBodyPart createMimeBodyPart(InputStream in) throws MessagingException {
419            return new MimeBodyPart(in);
420        }
421    
422        // static used to track boudary value allocations to help ensure uniqueness.
423        private static int part;
424    
425        private synchronized static String getBoundary() {
426            int i;
427            synchronized(MimeMultipart.class) {
428                i = part++;
429            }
430            StringBuffer buf = new StringBuffer(64);
431            buf.append("----=_Part_").append(i).append('_').append((new Object()).hashCode()).append('.').append(System.currentTimeMillis());
432            return buf.toString();
433        }
434    
435        private class MimeBodyPartInputStream extends InputStream {
436            BufferedInputStream inStream;
437            public boolean boundaryFound = false;
438            byte[] boundary;
439            public boolean finalBoundaryFound = false; 
440    
441            public MimeBodyPartInputStream(BufferedInputStream inStream, byte[] boundary) {
442                super();
443                this.inStream = inStream;
444                this.boundary = boundary;
445            }
446    
447            /**
448             * The base reading method for reading one character 
449             * at a time. 
450             * 
451             * @return The read character, or -1 if an EOF was encountered. 
452             * @exception IOException
453             */
454            public int read() throws IOException {
455                if (boundaryFound) {
456                    return -1;
457                }
458                
459                // read the next value from stream
460                int firstChar = inStream.read();
461                // premature end?  Handle it like a boundary located 
462                if (firstChar == -1) {
463                    boundaryFound = true; 
464                    // also mark this as the end 
465                    finalBoundaryFound = true; 
466                    return -1; 
467                }
468                
469                // we first need to look for a line boundary.  If we find a boundary, it can be followed by the 
470                // boundary marker, so we need to remember what sort of thing we found, then read ahead looking 
471                // for the part boundary. 
472                
473                // NB:, we only handle [\r]\n--boundary marker[--]
474                // we need to at least accept what most mail servers would consider an 
475                // invalid format using just '\n'
476                if (firstChar != '\r' && firstChar != '\n') {
477                    // not a \r, just return the byte as is 
478                    return firstChar;
479                }
480                // we might need to rewind to this point.  The padding is to allow for 
481                // line terminators and linear whitespace on the boundary lines 
482                inStream.mark(boundary.length + 1000); 
483                // we need to keep track of the first read character in case we need to 
484                // rewind back to the mark point 
485                int value = firstChar; 
486                // if this is a '\r', then we require the '\n'
487                if (value == '\r') {
488                    // now scan ahead for the second character 
489                    value = inStream.read();
490                    if (value != '\n') {
491                        // only a \r, so this can't be a boundary.  Return the 
492                        // \r as if it was data, after first resetting  
493                        inStream.reset(); 
494                        return '\r';
495                    } 
496                } 
497                
498                value = inStream.read();
499                // if the next character is not a boundary start, we 
500                // need to handle this as a normal line end 
501                if ((byte) value != boundary[0]) {
502                    // just reset and return the first character as data 
503                    inStream.reset(); 
504                    return firstChar; 
505                }
506                
507                // we're here because we found a "\r\n-" sequence, which is a potential 
508                // boundary marker.  Read the individual characters of the next line until 
509                // we have a mismatch 
510                
511                // read value is the first byte of the boundary. Start matching the
512                // next characters to find a boundary
513                int boundaryIndex = 0;
514                while ((boundaryIndex < boundary.length) && ((byte) value == boundary[boundaryIndex])) {
515                    value = inStream.read();
516                    boundaryIndex++;
517                }
518                // if we didn't match all the way, we need to push back what we've read and 
519                // return the EOL character 
520                if (boundaryIndex != boundary.length) { 
521                    // Boundary not found. Restoring bytes skipped.
522                    // just reset and return the first character as data 
523                    inStream.reset(); 
524                    return firstChar; 
525                }
526                
527                // The full boundary sequence should be \r\n--boundary string[--]\r\n
528                // if the last character we read was a '-', check for the end terminator 
529                if (value == '-') {
530                    value = inStream.read();
531                    // crud, we have a bad boundary terminator.  We need to unwind this all the way 
532                    // back to the lineend and pretend none of this ever happened
533                    if (value != '-') {
534                        // Boundary not found. Restoring bytes skipped.
535                        // just reset and return the first character as data 
536                        inStream.reset(); 
537                        return firstChar; 
538                    }
539                    // on the home stretch, but we need to verify the LWSP/EOL sequence 
540                    value = inStream.read();
541                    // first skip over the linear whitespace 
542                    while (value == ' ' || value == '\t') {
543                        value = inStream.read();
544                    }
545                    
546                    // We've matched the final boundary, skipped any whitespace, but 
547                    // we've hit the end of the stream.  This is highly likely when 
548                    // we have nested multiparts, since the linend terminator for the 
549                    // final boundary marker is eated up as the start of the outer 
550                    // boundary marker.  No CRLF sequence here is ok. 
551                    if (value == -1) {
552                        // we've hit the end of times...
553                        finalBoundaryFound = true; 
554                        // we have a boundary, so return this as an EOF condition 
555                        boundaryFound = true;
556                        return -1;
557                    }
558                    
559                    // this must be a CR or a LF...which leaves us even more to push back and forget 
560                    if (value != '\r' && value != '\n') {
561                        // Boundary not found. Restoring bytes skipped.
562                        // just reset and return the first character as data 
563                        inStream.reset(); 
564                        return firstChar; 
565                    }
566                    
567                    // if this is carriage return, check for a linefeed  
568                    if (value == '\r') {
569                        // last check, this must be a line feed 
570                        value = inStream.read();
571                        if (value != '\n') {
572                            // SO CLOSE!
573                            // Boundary not found. Restoring bytes skipped.
574                            // just reset and return the first character as data 
575                            inStream.reset(); 
576                            return firstChar; 
577                        }
578                    }
579                    
580                    // we've hit the end of times...
581                    finalBoundaryFound = true; 
582                }
583                else {
584                    // first skip over the linear whitespace 
585                    while (value == ' ' || value == '\t') {
586                        value = inStream.read();
587                    }
588                    // this must be a CR or a LF...which leaves us even more to push back and forget 
589                    if (value != '\r' && value != '\n') {
590                        // Boundary not found. Restoring bytes skipped.
591                        // just reset and return the first character as data 
592                        inStream.reset(); 
593                        return firstChar; 
594                    }
595                    
596                    // if this is carriage return, check for a linefeed  
597                    if (value == '\r') {
598                        // last check, this must be a line feed 
599                        value = inStream.read();
600                        if (value != '\n') {
601                            // SO CLOSE!
602                            // Boundary not found. Restoring bytes skipped.
603                            // just reset and return the first character as data 
604                            inStream.reset(); 
605                            return firstChar; 
606                        }
607                    }
608                }
609                // we have a boundary, so return this as an EOF condition 
610                boundaryFound = true;
611                return -1;
612            }
613        }
614    
615        
616        /**
617         * Return true if the final boundary line for this multipart was
618         * seen when parsing the data.
619         *
620         * @return
621         * @exception MessagingException
622         */
623        public boolean isComplete() throws MessagingException {
624            // make sure we've parsed this
625            parse();
626            return complete;
627        }
628    
629    
630        /**
631         * Returns the preamble text that appears before the first bady
632         * part of a MIME multi part.  The preamble is optional, so this
633         * might be null.
634         *
635         * @return The preamble text string.
636         * @exception MessagingException
637         */
638        public String getPreamble() throws MessagingException {
639            parse();
640            return preamble;
641        }
642    
643        /**
644         * Set the message preamble text.  This will be written before
645         * the first boundary of a multi-part message.
646         *
647         * @param preamble The new boundary text.  This is complete lines of text, including
648         *                 new lines.
649         *
650         * @exception MessagingException
651         */
652        public void setPreamble(String preamble) throws MessagingException {
653            this.preamble = preamble;
654        }
655    }