/*
 * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.jvnet.mimepull;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

Represents MIME message. MIME message parsing is done lazily using a pull parser.
Author:Jitendra Kotamraju
/** * Represents MIME message. MIME message parsing is done lazily using a * pull parser. * * @author Jitendra Kotamraju */
public class MIMEMessage implements Closeable { private static final Logger LOGGER = Logger.getLogger(MIMEMessage.class.getName()); MIMEConfig config; private final InputStream in; private final Iterator<MIMEEvent> it; private boolean parsed; // true when entire message is parsed private MIMEPart currentPart; private int currentIndex; private final List<MIMEPart> partsList = new ArrayList<MIMEPart>(); private final Map<String, MIMEPart> partsMap = new HashMap<String, MIMEPart>();
Params:
  • in – MIME message stream
  • boundary – the separator for parts(pass it without --)
See Also:
  • MIMEMessage(InputStream, String, MIMEConfig)
/** * @see #MIMEMessage(InputStream, String, MIMEConfig) * * @param in MIME message stream * @param boundary the separator for parts(pass it without --) */
public MIMEMessage(InputStream in, String boundary) { this(in, boundary, new MIMEConfig()); }
Creates a MIME message from the content's stream. The content stream is closed when EOF is reached.
Params:
  • in – MIME message stream
  • boundary – the separator for parts(pass it without --)
  • config – various configuration parameters
/** * Creates a MIME message from the content's stream. The content stream * is closed when EOF is reached. * * @param in MIME message stream * @param boundary the separator for parts(pass it without --) * @param config various configuration parameters */
public MIMEMessage(InputStream in, String boundary, MIMEConfig config) { this.in = in; this.config = config; MIMEParser parser = new MIMEParser(in, boundary, config); it = parser.iterator(); if (config.isParseEagerly()) { parseAll(); } }
Gets all the attachments by parsing the entire MIME message. Avoid this if possible since it is an expensive operation.
Returns:list of attachments.
/** * Gets all the attachments by parsing the entire MIME message. Avoid * this if possible since it is an expensive operation. * * @return list of attachments. */
public List<MIMEPart> getAttachments() { if (!parsed) { parseAll(); } return partsList; }
Creates nth attachment lazily. It doesn't validate if the message has so many attachments. To do the validation, the message needs to be parsed. The parsing of the message is done lazily and is done while reading the bytes of the part.
Params:
  • index – sequential order of the part. starts with zero.
Returns:attachemnt part
/** * Creates nth attachment lazily. It doesn't validate * if the message has so many attachments. To * do the validation, the message needs to be parsed. * The parsing of the message is done lazily and is done * while reading the bytes of the part. * * @param index sequential order of the part. starts with zero. * @return attachemnt part */
public MIMEPart getPart(int index) { LOGGER.log(Level.FINE, "index={0}", index); MIMEPart part = (index < partsList.size()) ? partsList.get(index) : null; if (parsed && part == null) { throw new MIMEParsingException("There is no " + index + " attachment part "); } if (part == null) { // Parsing will done lazily and will be driven by reading the part part = new MIMEPart(this); partsList.add(index, part); } LOGGER.log(Level.FINE, "Got attachment at index={0} attachment={1}", new Object[] {index, part}); return part; }
Creates a lazy attachment for a given Content-ID. It doesn't validate if the message contains an attachment with the given Content-ID. To do the validation, the message needs to be parsed. The parsing of the message is done lazily and is done while reading the bytes of the part.
Params:
  • contentId – Content-ID of the part, expects Content-ID without <, >
Returns:attachemnt part
/** * Creates a lazy attachment for a given Content-ID. It doesn't validate * if the message contains an attachment with the given Content-ID. To * do the validation, the message needs to be parsed. The parsing of the * message is done lazily and is done while reading the bytes of the part. * * @param contentId Content-ID of the part, expects Content-ID without {@code <, >} * @return attachemnt part */
public MIMEPart getPart(String contentId) { LOGGER.log(Level.FINE, "Content-ID={0}", contentId); MIMEPart part = getDecodedCidPart(contentId); if (parsed && part == null) { throw new MIMEParsingException("There is no attachment part with Content-ID = " + contentId); } if (part == null) { // Parsing is done lazily and is driven by reading the part part = new MIMEPart(this, contentId); partsMap.put(contentId, part); } LOGGER.log(Level.FINE, "Got attachment for Content-ID={0} attachment={1}", new Object[] {contentId, part}); return part; } // this is required for Indigo interop, it writes content-id without escaping private MIMEPart getDecodedCidPart(String cid) { MIMEPart part = partsMap.get(cid); if (part == null) { if (cid.indexOf('%') != -1) { try { String tempCid = URLDecoder.decode(cid, "utf-8"); part = partsMap.get(tempCid); } catch (UnsupportedEncodingException ue) { // Ignore it } } } return part; }
Parses the whole MIME message eagerly
/** * Parses the whole MIME message eagerly */
public final void parseAll() { while (makeProgress()) { // Nothing to do } }
Closes all parsed parts. This method is safe to call even if parsing of message failed.

Does not throw MIMEParsingException if an error occurred during closing a MIME part. The exception (if any) is still logged.

/** * Closes all parsed {@link org.jvnet.mimepull.MIMEPart parts}. * This method is safe to call even if parsing of message failed. * * <p> Does not throw {@link org.jvnet.mimepull.MIMEParsingException} if an * error occurred during closing a MIME part. The exception (if any) is * still logged. */
@Override public void close() { close(partsList); close(partsMap.values()); } private void close(final Collection<MIMEPart> parts) { for (final MIMEPart part : parts) { try { part.close(); } catch (final MIMEParsingException closeError) { LOGGER.log(Level.FINE, "Exception during closing MIME part", closeError); } } }
Parses the MIME message in a pull fashion.
Returns:false if the parsing is completed.
/** * Parses the MIME message in a pull fashion. * * @return false if the parsing is completed. */
public synchronized boolean makeProgress() { if (!it.hasNext()) { return false; } MIMEEvent event = it.next(); switch (event.getEventType()) { case START_MESSAGE: LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_MESSAGE); break; case START_PART: LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.START_PART); break; case HEADERS: LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.HEADERS); MIMEEvent.Headers headers = (MIMEEvent.Headers) event; InternetHeaders ih = headers.getHeaders(); List<String> cids = ih.getHeader("content-id"); String cid = (cids != null) ? cids.get(0) : currentIndex + ""; if (cid.length() > 2 && cid.charAt(0) == '<') { cid = cid.substring(1, cid.length() - 1); } MIMEPart listPart = (currentIndex < partsList.size()) ? partsList.get(currentIndex) : null; MIMEPart mapPart = getDecodedCidPart(cid); if (listPart == null && mapPart == null) { currentPart = getPart(cid); partsList.add(currentIndex, currentPart); } else if (listPart == null) { currentPart = mapPart; partsList.add(currentIndex, mapPart); } else if (mapPart == null) { currentPart = listPart; currentPart.setContentId(cid); partsMap.put(cid, currentPart); } else if (listPart != mapPart) { throw new MIMEParsingException("Created two different attachments using Content-ID and index"); } currentPart.setHeaders(ih); break; case CONTENT: LOGGER.log(Level.FINER, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.CONTENT); MIMEEvent.Content content = (MIMEEvent.Content) event; ByteBuffer buf = content.getData(); currentPart.addBody(buf); break; case END_PART: LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_PART); currentPart.doneParsing(); ++currentIndex; break; case END_MESSAGE: LOGGER.log(Level.FINE, "MIMEEvent={0}", MIMEEvent.EVENT_TYPE.END_MESSAGE); parsed = true; try { in.close(); } catch (IOException ioe) { throw new MIMEParsingException(ioe); } break; default: throw new MIMEParsingException("Unknown Parser state = " + event.getEventType()); } return true; } }