/*
* <strong>heavily modified version based on org.apache.commons.mail.util.MimeMessageParser.html</strong>
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.simplejavamail.converter.internal.mimemessage;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.util.ByteArrayDataSource;
import java.io.*;
import java.util.*;
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
/**
* <strong>heavily modified version based on org.apache.commons.mail.util.MimeMessageParser.html</strong>
* Parses a MimeMessage and stores the individual parts such a plain text, HTML text and attachments.
*
* @version current: MimeMessageParser.java 2016-02-25 Benny Bottema
*/
public class MimeMessageParser {
private static final List<String> DEFAULT_HEADERS = new ArrayList<>();
static {
// taken from: protected javax.mail.internet.InternetHeaders constructor
/*
* When extracting information to create an Email, we're not interested in the following headers:
*/
DEFAULT_HEADERS.add("Return-Path");
DEFAULT_HEADERS.add("Received");
DEFAULT_HEADERS.add("Resent-Date");
DEFAULT_HEADERS.add("Resent-From");
DEFAULT_HEADERS.add("Resent-Sender");
DEFAULT_HEADERS.add("Resent-To");
DEFAULT_HEADERS.add("Resent-Cc");
DEFAULT_HEADERS.add("Resent-Bcc");
DEFAULT_HEADERS.add("Resent-Message-Id");
DEFAULT_HEADERS.add("Date");
DEFAULT_HEADERS.add("From");
DEFAULT_HEADERS.add("Sender");
DEFAULT_HEADERS.add("Reply-To");
DEFAULT_HEADERS.add("To");
DEFAULT_HEADERS.add("Cc");
DEFAULT_HEADERS.add("Bcc");
DEFAULT_HEADERS.add("Message-Id");
DEFAULT_HEADERS.add("In-Reply-To");
DEFAULT_HEADERS.add("References");
DEFAULT_HEADERS.add("Subject");
DEFAULT_HEADERS.add("Comments");
DEFAULT_HEADERS.add("Keywords");
DEFAULT_HEADERS.add("Errors-To");
DEFAULT_HEADERS.add("MIME-Version");
DEFAULT_HEADERS.add("Content-Type");
DEFAULT_HEADERS.add("Content-Transfer-Encoding");
DEFAULT_HEADERS.add("Content-MD5");
DEFAULT_HEADERS.add(":");
DEFAULT_HEADERS.add("Content-Length");
DEFAULT_HEADERS.add("Status");
// extra headers that should be ignored, which may originate from nested attachments
DEFAULT_HEADERS.add("Content-Disposition");
DEFAULT_HEADERS.add("size");
DEFAULT_HEADERS.add("filename");
DEFAULT_HEADERS.add("Content-ID");
DEFAULT_HEADERS.add("name");
DEFAULT_HEADERS.add("From");
}
private final Map<String, DataSource> attachmentList = new HashMap<>();
private final Map<String, DataSource> cidMap = new HashMap<>();
private final Map<String, Object> headers = new HashMap<>();
private final MimeMessage mimeMessage;
private String plainContent;
private String htmlContent;
/**
* Constructs an instance with the MimeMessage to be extracted.
*
* @param message the message to parse
*/
public MimeMessageParser(final MimeMessage message) {
this.mimeMessage = message;
}
/**
* Does the actual extraction.
*
* @return this instance
*/
public MimeMessageParser parse()
throws MessagingException, IOException {
this.parse(mimeMessage);
return this;
}
/**
* @return the 'to' recipients of the message
*/
public List<InternetAddress> getTo()
throws MessagingException {
return getInternetAddresses(this.mimeMessage.getRecipients(Message.RecipientType.TO));
}
/**
* @return the 'cc' recipients of the message
*/
public List<InternetAddress> getCc()
throws MessagingException {
return getInternetAddresses(this.mimeMessage.getRecipients(Message.RecipientType.CC));
}
/**
* @return the 'bcc' recipients of the message
*/
public List<InternetAddress> getBcc()
throws MessagingException {
return getInternetAddresses(this.mimeMessage.getRecipients(Message.RecipientType.BCC));
}
private static List<InternetAddress> getInternetAddresses(final Address[] recipients) {
final List<Address> addresses = (recipients != null) ? Arrays.asList(recipients) : new ArrayList<Address>();
final List<InternetAddress> mailAddresses = new ArrayList<>();
for (final Address address : addresses) {
if (address instanceof InternetAddress) {
mailAddresses.add((InternetAddress) address);
}
}
return mailAddresses;
}
/**
* @return the 'from' field of the message
*/
public InternetAddress getFrom()
throws MessagingException {
final Address[] addresses = this.mimeMessage.getFrom();
if (addresses == null || addresses.length == 0) {
return null;
}
return (InternetAddress) addresses[0];
}
/**
* @return the 'replyTo' address of the email
*/
public InternetAddress getReplyTo()
throws MessagingException {
final Address[] addresses = this.mimeMessage.getReplyTo();
if (addresses == null || addresses.length == 0) {
return null;
}
return (InternetAddress) addresses[0];
}
/**
* @return the mail subject
*/
public String getSubject()
throws MessagingException {
return this.mimeMessage.getSubject();
}
/**
* Extracts the content of a MimeMessage recursively.
*
* @param part the current MimePart
* @throws MessagingException parsing the MimeMessage failed
* @throws IOException parsing the MimeMessage failed
*/
private void parse(final MimePart part)
throws MessagingException, IOException {
extractCustomUserHeaders(part);
if (isMimeType(part, "text/plain") && plainContent == null
&& !Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
plainContent = (String) part.getContent();
} else {
if (isMimeType(part, "text/html") && htmlContent == null
&& !Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
htmlContent = (String) part.getContent();
} else {
if (isMimeType(part, "multipart/*")) {
final Multipart mp = (Multipart) part.getContent();
final int count = mp.getCount();
// iterate over all MimeBodyPart
for (int i = 0; i < count; i++) {
parse((MimeBodyPart) mp.getBodyPart(i));
}
} else {
final DataSource ds = createDataSource(part);
// If the diposition is not provided, the part should be treat as attachment
if (part.getDisposition() == null || Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
this.attachmentList.put(parseResourceName(part.getContentID(), part.getFileName()), ds);
} else if (Part.INLINE.equalsIgnoreCase(part.getDisposition())) {
this.cidMap.put(part.getContentID(), ds);
} else {
throw new IllegalStateException("invalid attachment type");
}
}
}
}
}
private static String parseResourceName(final String contentID, final String fileName) {
String extension = "";
if (!valueNullOrEmpty(fileName) && fileName.contains(".")) {
extension = fileName.substring(fileName.lastIndexOf("."), fileName.length());
}
if (!valueNullOrEmpty(contentID)) {
return (contentID.endsWith(extension)) ? contentID : contentID + extension;
} else {
return fileName;
}
}
private void extractCustomUserHeaders(final MimePart part)
throws MessagingException {
final Enumeration e = part.getAllHeaders();
while (e.hasMoreElements()) {
final Object headerObj = e.nextElement();
if (headerObj instanceof Header) {
final Header header = (Header) headerObj;
if (isCustomUserHeader(header)) {
headers.put(header.getName(), header.getValue());
}
}
}
}
private static boolean isCustomUserHeader(final Header header) {
return !DEFAULT_HEADERS.contains(header.getName());
}
/**
* Checks whether the MimePart contains an object of the given mime type.
*
* @param part the current MimePart
* @param mimeType the mime type to check
* @return {@code true} if the MimePart matches the given mime type, {@code false} otherwise
* @throws MessagingException parsing the MimeMessage failed
*/
private static boolean isMimeType(final MimePart part, final String mimeType)
throws MessagingException {
// Do not use part.isMimeType(String) as it is broken for MimeBodyPart
// and does not really check the actual content type.
try {
final ContentType ct = new ContentType(part.getDataHandler().getContentType());
return ct.match(mimeType);
} catch (final ParseException ex) {
return part.getContentType().equalsIgnoreCase(mimeType);
}
}
/**
* Parses the MimePart to create a DataSource.
*
* @param part the current part to be processed
* @return the DataSource
* @throws MessagingException creating the DataSource failed
* @throws IOException creating the DataSource failed
*/
private static DataSource createDataSource(final MimePart part)
throws MessagingException, IOException {
final DataHandler dataHandler = part.getDataHandler();
final DataSource dataSource = dataHandler.getDataSource();
final String contentType = getBaseMimeType(dataSource.getContentType());
final byte[] content = MimeMessageParser.getContent(dataSource.getInputStream());
final ByteArrayDataSource result = new ByteArrayDataSource(content, contentType);
final String dataSourceName = getDataSourceName(part, dataSource);
result.setName(dataSourceName);
return result;
}
/**
* Determines the name of the data source if it is not already set.
*
* @param part the mail part
* @param dataSource the data source
* @return the name of the data source or {@code null} if no name can be determined
* @throws MessagingException accessing the part failed
* @throws UnsupportedEncodingException decoding the text failed
*/
private static String getDataSourceName(final Part part, final DataSource dataSource)
throws MessagingException, UnsupportedEncodingException {
String result = dataSource.getName();
if (result == null || result.length() == 0) {
result = part.getFileName();
}
if (result != null && result.length() > 0) {
result = MimeUtility.decodeText(result);
} else {
result = null;
}
return result;
}
/**
* Read the content of the input stream.
*
* @param is the input stream to process
* @return the content of the input stream
* @throws IOException reading the input stream failed
*/
private static byte[] getContent(final InputStream is)
throws IOException {
int ch;
final byte[] result;
final ByteArrayOutputStream os = new ByteArrayOutputStream();
final BufferedInputStream isReader = new BufferedInputStream(is);
final BufferedOutputStream osWriter = new BufferedOutputStream(os);
while ((ch = isReader.read()) != -1) {
osWriter.write(ch);
}
osWriter.flush();
result = os.toByteArray();
osWriter.close();
return result;
}
/**
* Parses the mimeType.
*
* @param fullMimeType the mime type from the mail api
* @return the real mime type
*/
private static String getBaseMimeType(final String fullMimeType) {
final int pos = fullMimeType.indexOf(';');
if (pos >= 0) {
return fullMimeType.substring(0, pos);
}
return fullMimeType;
}
/**
* @return {@link #cidMap}
*/
public Map<String, DataSource> getCidMap() {
return cidMap;
}
/**
* @return {@link #headers}
*/
public Map<String, Object> getHeaders() {
return headers;
}
/**
* @return {@link #plainContent}
*/
public String getPlainContent() {
return plainContent;
}
/**
* @return {@link #attachmentList}
*/
public Map<String, DataSource> getAttachmentList() {
return attachmentList;
}
/**
* @return {@link #htmlContent}
*/
public String getHtmlContent() {
return htmlContent;
}
}