MIMEMessage.java revision 582:f31835b59035
11590Srgrimes/*
21590Srgrimes * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved.
31590Srgrimes * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
41590Srgrimes *
51590Srgrimes * This code is free software; you can redistribute it and/or modify it
61590Srgrimes * under the terms of the GNU General Public License version 2 only, as
71590Srgrimes * published by the Free Software Foundation.  Oracle designates this
81590Srgrimes * particular file as subject to the "Classpath" exception as provided
91590Srgrimes * by Oracle in the LICENSE file that accompanied this code.
101590Srgrimes *
111590Srgrimes * This code is distributed in the hope that it will be useful, but WITHOUT
121590Srgrimes * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
131590Srgrimes * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
141590Srgrimes * version 2 for more details (a copy is included in the LICENSE file that
151590Srgrimes * accompanied this code).
161590Srgrimes *
171590Srgrimes * You should have received a copy of the GNU General Public License version
181590Srgrimes * 2 along with this work; if not, write to the Free Software Foundation,
191590Srgrimes * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
201590Srgrimes *
211590Srgrimes * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
221590Srgrimes * or visit www.oracle.com if you need additional information or have any
231590Srgrimes * questions.
241590Srgrimes */
251590Srgrimes
261590Srgrimespackage com.sun.xml.internal.org.jvnet.mimepull;
271590Srgrimes
281590Srgrimesimport java.io.Closeable;
291590Srgrimesimport java.io.IOException;
301590Srgrimesimport java.io.InputStream;
311590Srgrimesimport java.io.UnsupportedEncodingException;
321590Srgrimesimport java.net.URLDecoder;
331590Srgrimesimport java.nio.ByteBuffer;
341590Srgrimesimport java.util.ArrayList;
351590Srgrimesimport java.util.Collection;
361590Srgrimesimport java.util.HashMap;
3762833Swsanchezimport java.util.Iterator;
3862833Swsanchezimport java.util.List;
391590Srgrimesimport java.util.Map;
401590Srgrimesimport java.util.logging.Level;
411590Srgrimesimport java.util.logging.Logger;
4262833Swsanchez
4362833Swsanchez/**
441590Srgrimes * Represents MIME message. MIME message parsing is done lazily using a
451590Srgrimes * pull parser.
461590Srgrimes *
471590Srgrimes * @author Jitendra Kotamraju
481590Srgrimes */
491590Srgrimespublic class MIMEMessage implements Closeable {
501590Srgrimes
511590Srgrimes    private static final Logger LOGGER = Logger.getLogger(MIMEMessage.class.getName());
521590Srgrimes
531590Srgrimes    MIMEConfig config;
541590Srgrimes
551590Srgrimes    private final InputStream in;
561590Srgrimes    private final Iterator<MIMEEvent> it;
571590Srgrimes    private boolean parsed;     // true when entire message is parsed
581590Srgrimes    private MIMEPart currentPart;
591590Srgrimes    private int currentIndex;
601590Srgrimes
611590Srgrimes    private final List<MIMEPart> partsList = new ArrayList<MIMEPart>();
621590Srgrimes    private final Map<String, MIMEPart> partsMap = new HashMap<String, MIMEPart>();
631590Srgrimes
641590Srgrimes    /**
651590Srgrimes     * @see MIMEMessage(InputStream, String, MIMEConfig)
661590Srgrimes     */
671590Srgrimes    public MIMEMessage(InputStream in, String boundary) {
681590Srgrimes        this(in, boundary, new MIMEConfig());
691590Srgrimes    }
701590Srgrimes
711590Srgrimes    /**
721590Srgrimes     * Creates a MIME message from the content's stream. The content stream
731590Srgrimes     * is closed when EOF is reached.
741590Srgrimes     *
751590Srgrimes     * @param in       MIME message stream
761590Srgrimes     * @param boundary the separator for parts(pass it without --)
771590Srgrimes     * @param config   various configuration parameters
781590Srgrimes     */
791590Srgrimes    public MIMEMessage(InputStream in, String boundary, MIMEConfig config) {
801590Srgrimes        this.in = in;
811590Srgrimes        this.config = config;
821590Srgrimes        MIMEParser parser = new MIMEParser(in, boundary, config);
831590Srgrimes        it = parser.iterator();
841590Srgrimes
851590Srgrimes        if (config.isParseEagerly()) {
861590Srgrimes            parseAll();
871590Srgrimes        }
881590Srgrimes    }
891590Srgrimes
905814Sjkh    /**
915814Sjkh     * Gets all the attachments by parsing the entire MIME message. Avoid
921590Srgrimes     * this if possible since it is an expensive operation.
931590Srgrimes     *
941590Srgrimes     * @return list of attachments.
951590Srgrimes     */
961590Srgrimes    public List<MIMEPart> getAttachments() {
975814Sjkh        if (!parsed) {
981590Srgrimes            parseAll();
991590Srgrimes        }
10018730Ssteve        return partsList;
1011590Srgrimes    }
1025814Sjkh
1031590Srgrimes    /**
1041590Srgrimes     * Creates nth attachment lazily. It doesn't validate
1051590Srgrimes     * if the message has so many attachments. To
1061590Srgrimes     * do the validation, the message needs to be parsed.
1071590Srgrimes     * The parsing of the message is done lazily and is done
1081590Srgrimes     * while reading the bytes of the part.
1091590Srgrimes     *
1101590Srgrimes     * @param index sequential order of the part. starts with zero.
1111590Srgrimes     * @return attachemnt part
1121590Srgrimes     */
1131590Srgrimes    public MIMEPart getPart(int index) {
11418730Ssteve        LOGGER.log(Level.FINE, "index={0}", index);
11518730Ssteve        MIMEPart part = (index < partsList.size()) ? partsList.get(index) : null;
1161590Srgrimes        if (parsed && part == null) {
1171590Srgrimes            throw new MIMEParsingException("There is no " + index + " attachment part ");
11869531Swill        }
11969531Swill        if (part == null) {
1201590Srgrimes            // Parsing will done lazily and will be driven by reading the part
1211590Srgrimes            part = new MIMEPart(this);
12251132Sjulian            partsList.add(index, part);
12318730Ssteve        }
12418730Ssteve        LOGGER.log(Level.FINE, "Got attachment at index={0} attachment={1}", new Object[] {index, part});
12518730Ssteve        return part;
1261590Srgrimes    }
1271590Srgrimes
1281590Srgrimes    /**
1295814Sjkh     * Creates a lazy attachment for a given Content-ID. It doesn't validate
1305814Sjkh     * if the message contains an attachment with the given Content-ID. To
1315814Sjkh     * do the validation, the message needs to be parsed. The parsing of the
1325814Sjkh     * message is done lazily and is done while reading the bytes of the part.
1335814Sjkh     *
1345814Sjkh     * @param contentId Content-ID of the part, expects Content-ID without <, >
1355814Sjkh     * @return attachemnt part
1365814Sjkh     */
1375814Sjkh    public MIMEPart getPart(String contentId) {
1385814Sjkh        LOGGER.log(Level.FINE, "Content-ID={0}", contentId);
1395814Sjkh        MIMEPart part = getDecodedCidPart(contentId);
1405814Sjkh        if (parsed && part == null) {
1415814Sjkh            throw new MIMEParsingException("There is no attachment part with Content-ID = " + contentId);
14269531Swill        }
1435814Sjkh        if (part == null) {
1445814Sjkh            // Parsing is done lazily and is driven by reading the part
1455814Sjkh            part = new MIMEPart(this, contentId);
1465814Sjkh            partsMap.put(contentId, part);
1478874Srgrimes        }
1488874Srgrimes        LOGGER.log(Level.FINE, "Got attachment for Content-ID={0} attachment={1}", new Object[] {contentId, part});
1495814Sjkh        return part;
15049938Shoek    }
1515814Sjkh
15269531Swill    // this is required for Indigo interop, it writes content-id without escaping
1535814Sjkh    private MIMEPart getDecodedCidPart(String cid) {
1545814Sjkh        MIMEPart part = partsMap.get(cid);
15549938Shoek        if (part == null) {
1565814Sjkh            if (cid.indexOf('%') != -1) {
15769531Swill                try {
1585814Sjkh                    String tempCid = URLDecoder.decode(cid, "utf-8");
1595814Sjkh                    part = partsMap.get(tempCid);
1605814Sjkh                } catch (UnsupportedEncodingException ue) {
1618874Srgrimes                    // Ignore it
1625814Sjkh                }
1635814Sjkh            }
1641590Srgrimes        }
1651590Srgrimes        return part;
1661590Srgrimes    }
1671590Srgrimes
1681590Srgrimes    /**
1691590Srgrimes     * Parses the whole MIME message eagerly
1701590Srgrimes     */
1711590Srgrimes    public final void parseAll() {
1721590Srgrimes        while (makeProgress()) {
1731590Srgrimes            // Nothing to do
1741590Srgrimes        }
1751590Srgrimes    }
1761590Srgrimes
1771590Srgrimes    /**
1781590Srgrimes     * Closes all parsed {@link com.sun.xml.internal.org.jvnet.mimepull.MIMEPart parts}.
1791590Srgrimes     * This method is safe to call even if parsing of message failed.
1801590Srgrimes     * <p/>
1811590Srgrimes     * Does not throw {@link com.sun.xml.internal.org.jvnet.mimepull.MIMEParsingException} if an
1821590Srgrimes     * error occurred during closing a MIME part. The exception (if any) is
1831590Srgrimes     * still logged.
1841590Srgrimes     */
1851590Srgrimes    @Override
1861590Srgrimes    public void close() {
1871590Srgrimes        close(partsList);
1881590Srgrimes        close(partsMap.values());
18969390Swill    }
1901590Srgrimes
1911590Srgrimes    private void close(final Collection<MIMEPart> parts) {
1921590Srgrimes        for (final MIMEPart part : parts) {
1931590Srgrimes            try {
1941590Srgrimes                part.close();
1958874Srgrimes            } catch (final MIMEParsingException closeError) {
1961590Srgrimes                LOGGER.log(Level.FINE, "Exception during closing MIME part", closeError);
1971590Srgrimes            }
1981590Srgrimes        }
1991590Srgrimes    }
2001590Srgrimes
2011590Srgrimes    /**
2021590Srgrimes     * Parses the MIME message in a pull fashion.
2031590Srgrimes     *
2041590Srgrimes     * @return false if the parsing is completed.
2051590Srgrimes     */
2061590Srgrimes    public synchronized boolean makeProgress() {
2078874Srgrimes        if (!it.hasNext()) {
2081590Srgrimes            return false;
2091590Srgrimes        }
2101590Srgrimes
2111590Srgrimes        MIMEEvent event = it.next();
2121590Srgrimes
2131590Srgrimes        switch (event.getEventType()) {
2148874Srgrimes            case START_MESSAGE:
2151590Srgrimes                LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_MESSAGE);
2161590Srgrimes                break;
2171590Srgrimes
2181590Srgrimes            case START_PART:
2191590Srgrimes                LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_PART);
2201590Srgrimes                break;
2211590Srgrimes
2221590Srgrimes            case HEADERS:
2231590Srgrimes                LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.HEADERS);
2241590Srgrimes                MIMEEvent.Headers headers = (MIMEEvent.Headers) event;
2251590Srgrimes                InternetHeaders ih = headers.getHeaders();
2261590Srgrimes                List<String> cids = ih.getHeader("content-id");
2278874Srgrimes                String cid = (cids != null) ? cids.get(0) : currentIndex + "";
2281590Srgrimes                if (cid.length() > 2 && cid.charAt(0) == '<') {
2291590Srgrimes                    cid = cid.substring(1, cid.length() - 1);
2301590Srgrimes                }
2311590Srgrimes                MIMEPart listPart = (currentIndex < partsList.size()) ? partsList.get(currentIndex) : null;
2321590Srgrimes                MIMEPart mapPart = getDecodedCidPart(cid);
2331590Srgrimes                if (listPart == null && mapPart == null) {
2341590Srgrimes                    currentPart = getPart(cid);
2351590Srgrimes                    partsList.add(currentIndex, currentPart);
2361590Srgrimes                } else if (listPart == null) {
2371590Srgrimes                    currentPart = mapPart;
2381590Srgrimes                    partsList.add(currentIndex, mapPart);
2391590Srgrimes                } else if (mapPart == null) {
2401590Srgrimes                    currentPart = listPart;
2411590Srgrimes                    currentPart.setContentId(cid);
2421590Srgrimes                    partsMap.put(cid, currentPart);
2431590Srgrimes                } else if (listPart != mapPart) {
2441590Srgrimes                    throw new MIMEParsingException("Created two different attachments using Content-ID and index");
2451590Srgrimes                }
2461590Srgrimes                currentPart.setHeaders(ih);
2471590Srgrimes                break;
2481590Srgrimes
2491590Srgrimes            case CONTENT:
2501590Srgrimes                LOGGER.log(Level.FINER, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.CONTENT);
2511590Srgrimes                MIMEEvent.Content content = (MIMEEvent.Content) event;
2521590Srgrimes                ByteBuffer buf = content.getData();
2531590Srgrimes                currentPart.addBody(buf);
2541590Srgrimes                break;
2551590Srgrimes
2561590Srgrimes            case END_PART:
2571590Srgrimes                LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_PART);
2581590Srgrimes                currentPart.doneParsing();
2591590Srgrimes                ++currentIndex;
2601590Srgrimes                break;
2611590Srgrimes
2621590Srgrimes            case END_MESSAGE:
2631590Srgrimes                LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_MESSAGE);
2641590Srgrimes                parsed = true;
2651590Srgrimes                try {
2661590Srgrimes                    in.close();
2671590Srgrimes                } catch (IOException ioe) {
2681590Srgrimes                    throw new MIMEParsingException(ioe);
2691590Srgrimes                }
2701590Srgrimes                break;
2711590Srgrimes
2721590Srgrimes            default:
2731590Srgrimes                throw new MIMEParsingException("Unknown Parser state = " + event.getEventType());
2741590Srgrimes        }
2751590Srgrimes        return true;
2761590Srgrimes    }
2771590Srgrimes}
2781590Srgrimes