1 /* 2 * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.xml.internal.org.jvnet.mimepull; 27 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.UnsupportedEncodingException; 31 import java.net.URLDecoder; 32 import java.nio.ByteBuffer; 33 import java.util.*; 34 import java.util.logging.Level; 35 import java.util.logging.Logger; 36 37 /** 38 * Represents MIME message. MIME message parsing is done lazily using a 39 * pull parser. 40 * 41 * @author Jitendra Kotamraju 42 */ 43 public class MIMEMessage { 44 private static final Logger LOGGER = Logger.getLogger(MIMEMessage.class.getName()); 45 46 MIMEConfig config; 47 48 private final InputStream in; 49 private final List<MIMEPart> partsList; 50 private final Map<String, MIMEPart> partsMap; 51 private final Iterator<MIMEEvent> it; 52 private boolean parsed; // true when entire message is parsed 53 private MIMEPart currentPart; 54 private int currentIndex; 55 56 /** 57 * @see MIMEMessage(InputStream, String, MIMEConfig) 58 */ MIMEMessage(InputStream in, String boundary)59 public MIMEMessage(InputStream in, String boundary) { 60 this(in, boundary, new MIMEConfig()); 61 } 62 63 /** 64 * Creates a MIME message from the content's stream. The content stream 65 * is closed when EOF is reached. 66 * 67 * @param in MIME message stream 68 * @param boundary the separator for parts(pass it without --) 69 * @param config various configuration parameters 70 */ MIMEMessage(InputStream in, String boundary, MIMEConfig config)71 public MIMEMessage(InputStream in, String boundary, MIMEConfig config) { 72 this.in = in; 73 this.config = config; 74 MIMEParser parser = new MIMEParser(in, boundary, config); 75 it = parser.iterator(); 76 77 partsList = new ArrayList<MIMEPart>(); 78 partsMap = new HashMap<String, MIMEPart>(); 79 if (config.isParseEagerly()) { 80 parseAll(); 81 } 82 } 83 84 /** 85 * Gets all the attachments by parsing the entire MIME message. Avoid 86 * this if possible since it is an expensive operation. 87 * 88 * @return list of attachments. 89 */ getAttachments()90 public List<MIMEPart> getAttachments() { 91 if (!parsed) { 92 parseAll(); 93 } 94 return partsList; 95 } 96 97 /** 98 * Creates nth attachment lazily. It doesn't validate 99 * if the message has so many attachments. To 100 * do the validation, the message needs to be parsed. 101 * The parsing of the message is done lazily and is done 102 * while reading the bytes of the part. 103 * 104 * @param index sequential order of the part. starts with zero. 105 * @return attachemnt part 106 */ getPart(int index)107 public MIMEPart getPart(int index) { 108 LOGGER.log(Level.FINE, "index={0}", index); 109 MIMEPart part = (index < partsList.size()) ? partsList.get(index) : null; 110 if (parsed && part == null) { 111 throw new MIMEParsingException("There is no "+index+" attachment part "); 112 } 113 if (part == null) { 114 // Parsing will done lazily and will be driven by reading the part 115 part = new MIMEPart(this); 116 partsList.add(index, part); 117 } 118 LOGGER.log(Level.FINE, "Got attachment at index={0} attachment={1}", new Object[]{index, part}); 119 return part; 120 } 121 122 /** 123 * Creates a lazy attachment for a given Content-ID. It doesn't validate 124 * if the message contains an attachment with the given Content-ID. To 125 * do the validation, the message needs to be parsed. The parsing of the 126 * message is done lazily and is done while reading the bytes of the part. 127 * 128 * @param contentId Content-ID of the part, expects Content-ID without <, > 129 * @return attachemnt part 130 */ getPart(String contentId)131 public MIMEPart getPart(String contentId) { 132 LOGGER.log(Level.FINE, "Content-ID={0}", contentId); 133 MIMEPart part = getDecodedCidPart(contentId); 134 if (parsed && part == null) { 135 throw new MIMEParsingException("There is no attachment part with Content-ID = "+contentId); 136 } 137 if (part == null) { 138 // Parsing is done lazily and is driven by reading the part 139 part = new MIMEPart(this, contentId); 140 partsMap.put(contentId, part); 141 } 142 LOGGER.log(Level.FINE, "Got attachment for Content-ID={0} attachment={1}", new Object[]{contentId, part}); 143 return part; 144 } 145 146 // this is required for Indigo interop, it writes content-id without escaping getDecodedCidPart(String cid)147 private MIMEPart getDecodedCidPart(String cid) { 148 MIMEPart part = partsMap.get(cid); 149 if (part == null) { 150 if (cid.indexOf('%') != -1) { 151 try { 152 String tempCid = URLDecoder.decode(cid, "utf-8"); 153 part = partsMap.get(tempCid); 154 } catch(UnsupportedEncodingException ue) { 155 // Ignore it 156 } 157 } 158 } 159 return part; 160 } 161 162 163 /** 164 * Parses the whole MIME message eagerly 165 */ parseAll()166 public final void parseAll() { 167 while(makeProgress()) { 168 // Nothing to do 169 } 170 } 171 172 173 /** 174 * Parses the MIME message in a pull fashion. 175 * 176 * @return 177 * false if the parsing is completed. 178 */ makeProgress()179 public synchronized boolean makeProgress() { 180 if (!it.hasNext()) { 181 return false; 182 } 183 184 MIMEEvent event = it.next(); 185 186 switch(event.getEventType()) { 187 case START_MESSAGE : 188 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_MESSAGE); 189 break; 190 191 case START_PART : 192 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_PART); 193 break; 194 195 case HEADERS : 196 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.HEADERS); 197 MIMEEvent.Headers headers = (MIMEEvent.Headers)event; 198 InternetHeaders ih = headers.getHeaders(); 199 List<String> cids = ih.getHeader("content-id"); 200 String cid = (cids != null) ? cids.get(0) : currentIndex+""; 201 if (cid.length() > 2 && cid.charAt(0)=='<') { 202 cid = cid.substring(1,cid.length()-1); 203 } 204 MIMEPart listPart = (currentIndex < partsList.size()) ? partsList.get(currentIndex) : null; 205 MIMEPart mapPart = getDecodedCidPart(cid); 206 if (listPart == null && mapPart == null) { 207 currentPart = getPart(cid); 208 partsList.add(currentIndex, currentPart); 209 } else if (listPart == null) { 210 currentPart = mapPart; 211 partsList.add(currentIndex, mapPart); 212 } else if (mapPart == null) { 213 currentPart = listPart; 214 currentPart.setContentId(cid); 215 partsMap.put(cid, currentPart); 216 } else if (listPart != mapPart) { 217 throw new MIMEParsingException("Created two different attachments using Content-ID and index"); 218 } 219 currentPart.setHeaders(ih); 220 break; 221 222 case CONTENT : 223 LOGGER.log(Level.FINER, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.CONTENT); 224 MIMEEvent.Content content = (MIMEEvent.Content)event; 225 ByteBuffer buf = content.getData(); 226 currentPart.addBody(buf); 227 break; 228 229 case END_PART : 230 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_PART); 231 currentPart.doneParsing(); 232 ++currentIndex; 233 break; 234 235 case END_MESSAGE : 236 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_MESSAGE); 237 parsed = true; 238 try { 239 in.close(); 240 } catch(IOException ioe) { 241 throw new MIMEParsingException(ioe); 242 } 243 break; 244 245 default : 246 throw new MIMEParsingException("Unknown Parser state = "+event.getEventType()); 247 } 248 return true; 249 } 250 } 251