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