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