1 /* 2 * Copyright (c) 1997, 2015, 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.Closeable; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.UnsupportedEncodingException; 32 import java.net.URLDecoder; 33 import java.nio.ByteBuffer; 34 import java.util.ArrayList; 35 import java.util.Collection; 36 import java.util.HashMap; 37 import java.util.Iterator; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.logging.Level; 41 import java.util.logging.Logger; 42 43 /** 44 * Represents MIME message. MIME message parsing is done lazily using a 45 * pull parser. 46 * 47 * @author Jitendra Kotamraju 48 */ 49 public class MIMEMessage implements Closeable { 50 51 private static final Logger LOGGER = Logger.getLogger(MIMEMessage.class.getName()); 52 53 MIMEConfig config; 54 55 private final InputStream in; 56 private final Iterator<MIMEEvent> it; 57 private boolean parsed; // true when entire message is parsed 58 private MIMEPart currentPart; 59 private int currentIndex; 60 61 private final List<MIMEPart> partsList = new ArrayList<MIMEPart>(); 62 private final Map<String, MIMEPart> partsMap = new HashMap<String, MIMEPart>(); 63 64 /** 65 * @see MIMEMessage(InputStream, String, MIMEConfig) 66 */ 67 public MIMEMessage(InputStream in, String boundary) { 68 this(in, boundary, new MIMEConfig()); 69 } 70 71 /** 72 * Creates a MIME message from the content's stream. The content stream 73 * is closed when EOF is reached. 74 * 75 * @param in MIME message stream 76 * @param boundary the separator for parts(pass it without --) 77 * @param config various configuration parameters 78 */ 79 public MIMEMessage(InputStream in, String boundary, MIMEConfig config) { 80 this.in = in; 81 this.config = config; 82 MIMEParser parser = new MIMEParser(in, boundary, config); 83 it = parser.iterator(); 84 85 if (config.isParseEagerly()) { 86 parseAll(); 87 } 88 } 89 90 /** 91 * Gets all the attachments by parsing the entire MIME message. Avoid 92 * this if possible since it is an expensive operation. 93 * 94 * @return list of attachments. 95 */ 96 public List<MIMEPart> getAttachments() { 97 if (!parsed) { 98 parseAll(); 99 } 100 return partsList; 101 } 102 103 /** 104 * Creates nth attachment lazily. It doesn't validate 105 * if the message has so many attachments. To 106 * do the validation, the message needs to be parsed. 107 * The parsing of the message is done lazily and is done 108 * while reading the bytes of the part. 109 * 110 * @param index sequential order of the part. starts with zero. 111 * @return attachemnt part 112 */ 113 public MIMEPart getPart(int index) { 114 LOGGER.log(Level.FINE, "index={0}", index); 115 MIMEPart part = (index < partsList.size()) ? partsList.get(index) : null; 116 if (parsed && part == null) { 117 throw new MIMEParsingException("There is no " + index + " attachment part "); 118 } 119 if (part == null) { 120 // Parsing will done lazily and will be driven by reading the part 121 part = new MIMEPart(this); 122 partsList.add(index, part); 123 } 124 LOGGER.log(Level.FINE, "Got attachment at index={0} attachment={1}", new Object[] {index, part}); 125 return part; 126 } 127 128 /** 129 * Creates a lazy attachment for a given Content-ID. It doesn't validate 130 * if the message contains an attachment with the given Content-ID. To 131 * do the validation, the message needs to be parsed. The parsing of the 132 * message is done lazily and is done while reading the bytes of the part. 133 * 134 * @param contentId Content-ID of the part, expects Content-ID without <, > 135 * @return attachemnt part 136 */ 137 public MIMEPart getPart(String contentId) { 138 LOGGER.log(Level.FINE, "Content-ID={0}", contentId); 139 MIMEPart part = getDecodedCidPart(contentId); 140 if (parsed && part == null) { 141 throw new MIMEParsingException("There is no attachment part with Content-ID = " + contentId); 142 } 143 if (part == null) { 144 // Parsing is done lazily and is driven by reading the part 145 part = new MIMEPart(this, contentId); 146 partsMap.put(contentId, part); 147 } 148 LOGGER.log(Level.FINE, "Got attachment for Content-ID={0} attachment={1}", new Object[] {contentId, part}); 149 return part; 150 } 151 152 // this is required for Indigo interop, it writes content-id without escaping 153 private MIMEPart getDecodedCidPart(String cid) { 154 MIMEPart part = partsMap.get(cid); 155 if (part == null) { 156 if (cid.indexOf('%') != -1) { 157 try { 158 String tempCid = URLDecoder.decode(cid, "utf-8"); 159 part = partsMap.get(tempCid); 160 } catch (UnsupportedEncodingException ue) { 161 // Ignore it 162 } 163 } 164 } 165 return part; 166 } 167 168 /** 169 * Parses the whole MIME message eagerly 170 */ 171 public final void parseAll() { 172 while (makeProgress()) { 173 // Nothing to do 174 } 175 } 176 177 /** 178 * Closes all parsed {@link com.sun.xml.internal.org.jvnet.mimepull.MIMEPart parts}. 179 * This method is safe to call even if parsing of message failed. 180 * <p/> 181 * Does not throw {@link com.sun.xml.internal.org.jvnet.mimepull.MIMEParsingException} if an 182 * error occurred during closing a MIME part. The exception (if any) is 183 * still logged. 184 */ 185 @Override 186 public void close() { 187 close(partsList); 188 close(partsMap.values()); 189 } 190 191 private void close(final Collection<MIMEPart> parts) { 192 for (final MIMEPart part : parts) { 193 try { 194 part.close(); 195 } catch (final MIMEParsingException closeError) { 196 LOGGER.log(Level.FINE, "Exception during closing MIME part", closeError); 197 } 198 } 199 } 200 201 /** 202 * Parses the MIME message in a pull fashion. 203 * 204 * @return false if the parsing is completed. 205 */ 206 public synchronized boolean makeProgress() { 207 if (!it.hasNext()) { 208 return false; 209 } 210 211 MIMEEvent event = it.next(); 212 213 switch (event.getEventType()) { 214 case START_MESSAGE: 215 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_MESSAGE); 216 break; 217 218 case START_PART: 219 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_PART); 220 break; 221 222 case HEADERS: 223 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.HEADERS); 224 MIMEEvent.Headers headers = (MIMEEvent.Headers) event; 225 InternetHeaders ih = headers.getHeaders(); 226 List<String> cids = ih.getHeader("content-id"); 227 String cid = (cids != null) ? cids.get(0) : currentIndex + ""; 228 if (cid.length() > 2 && cid.charAt(0) == '<') { 229 cid = cid.substring(1, cid.length() - 1); 230 } 231 MIMEPart listPart = (currentIndex < partsList.size()) ? partsList.get(currentIndex) : null; 232 MIMEPart mapPart = getDecodedCidPart(cid); 233 if (listPart == null && mapPart == null) { 234 currentPart = getPart(cid); 235 partsList.add(currentIndex, currentPart); 236 } else if (listPart == null) { 237 currentPart = mapPart; 238 partsList.add(currentIndex, mapPart); 239 } else if (mapPart == null) { 240 currentPart = listPart; 241 currentPart.setContentId(cid); 242 partsMap.put(cid, currentPart); 243 } else if (listPart != mapPart) { 244 throw new MIMEParsingException("Created two different attachments using Content-ID and index"); 245 } 246 currentPart.setHeaders(ih); 247 break; 248 249 case CONTENT: 250 LOGGER.log(Level.FINER, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.CONTENT); 251 MIMEEvent.Content content = (MIMEEvent.Content) event; 252 ByteBuffer buf = content.getData(); 253 currentPart.addBody(buf); 254 break; 255 256 case END_PART: 257 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_PART); 258 currentPart.doneParsing(); 259 ++currentIndex; 260 break; 261 262 case END_MESSAGE: 263 LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_MESSAGE); 264 parsed = true; 265 try { 266 in.close(); 267 } catch (IOException ioe) { 268 throw new MIMEParsingException(ioe); 269 } 270 break; 271 272 default: 273 throw new MIMEParsingException("Unknown Parser state = " + event.getEventType()); 274 } 275 return true; 276 } 277 }