/*
* (C) Copyright 2006-2009 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Nuxeo - initial API and implementation
*
* $Id$
*/
package org.nuxeo.ecm.platform.mail.listener.action;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.ATTACHMENTS_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.CC_RECIPIENTS_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.MIMETYPE_SERVICE_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.RECIPIENTS_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.SENDER_EMAIL_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.SENDER_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.SENDING_DATE_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.SUBJECT_KEY;
import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.TEXT_KEY;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.AddressException;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.platform.mail.action.ExecutionContext;
import org.nuxeo.ecm.platform.mail.utils.MailCoreConstants;
import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException;
import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
import org.nuxeo.runtime.api.Framework;
/**
* Puts on the pipe execution context the values retrieved from the new messages found in the INBOX. These values are
* used later when new MailMessage documents are created based on them.
*
* @author Catalin Baican
*/
public class ExtractMessageInformationAction extends AbstractMailAction {
private static final Log log = LogFactory.getLog(ExtractMessageInformationAction.class);
public static final String DEFAULT_BINARY_MIMETYPE = "application/octet-stream*";
public static final String MESSAGE_RFC822_MIMETYPE = "message/rfc822";
private String bodyContent;
public static final String COPY_MESSAGE = "org.nuxeo.mail.imap.copy";
@Override
public boolean execute(ExecutionContext context) {
bodyContent = "";
boolean copyMessage = Boolean.parseBoolean(Framework.getProperty(COPY_MESSAGE, "false"));
try {
Message originalMessage = context.getMessage();
if (log.isDebugEnabled()) {
log.debug("Transforming message, original subject: " + originalMessage.getSubject());
}
// fully load the message before trying to parse to
// override most of server bugs, see
// http://java.sun.com/products/javamail/FAQ.html#imapserverbug
Message message;
if (originalMessage instanceof MimeMessage && copyMessage) {
message = new MimeMessage((MimeMessage) originalMessage);
if (log.isDebugEnabled()) {
log.debug("Transforming message after full load: " + message.getSubject());
}
} else {
// stuck with the original one
message = originalMessage;
}
// Subject
String subject = message.getSubject();
if (subject != null) {
subject = subject.trim();
}
if (subject == null || "".equals(subject)) {
subject = "<Unknown>";
}
context.put(SUBJECT_KEY, subject);
// Sender
try {
Address[] from = message.getFrom();
String sender = null;
String senderEmail = null;
if (from != null) {
Address addr = from[0];
if (addr instanceof InternetAddress) {
InternetAddress iAddr = (InternetAddress) addr;
senderEmail = iAddr.getAddress();
sender = iAddr.getPersonal() + " <" + senderEmail + ">";
} else {
sender += addr.toString();
senderEmail = sender;
}
}
context.put(SENDER_KEY, sender);
context.put(SENDER_EMAIL_KEY, senderEmail);
} catch (AddressException ae) {
// try to parse sender from header instead
String[] values = message.getHeader("From");
if (values != null) {
context.put(SENDER_KEY, values[0]);
}
}
// Sending date
context.put(SENDING_DATE_KEY, message.getSentDate());
// Recipients
try {
Address[] to = message.getRecipients(Message.RecipientType.TO);
Collection<String> recipients = new ArrayList<String>();
if (to != null) {
for (Address addr : to) {
if (addr instanceof InternetAddress) {
InternetAddress iAddr = (InternetAddress) addr;
if (iAddr.getPersonal() != null) {
recipients.add(iAddr.getPersonal() + " <" + iAddr.getAddress() + ">");
} else {
recipients.add(iAddr.getAddress());
}
} else {
recipients.add(addr.toString());
}
}
}
context.put(RECIPIENTS_KEY, recipients);
} catch (AddressException ae) {
// try to parse recipient from header instead
Collection<String> recipients = getHeaderValues(message, Message.RecipientType.TO.toString());
context.put(RECIPIENTS_KEY, recipients);
}
// CC recipients
try {
Address[] toCC = message.getRecipients(Message.RecipientType.CC);
Collection<String> ccRecipients = new ArrayList<String>();
if (toCC != null) {
for (Address addr : toCC) {
if (addr instanceof InternetAddress) {
InternetAddress iAddr = (InternetAddress) addr;
ccRecipients.add(iAddr.getPersonal() + " " + iAddr.getAddress());
} else {
ccRecipients.add(addr.toString());
}
}
}
context.put(CC_RECIPIENTS_KEY, ccRecipients);
} catch (AddressException ae) {
// try to parse ccRecipient from header instead
Collection<String> ccRecipients = getHeaderValues(message, Message.RecipientType.CC.toString());
context.put(CC_RECIPIENTS_KEY, ccRecipients);
}
String[] messageIdHeader = message.getHeader("Message-ID");
if (messageIdHeader != null) {
context.put(MailCoreConstants.MESSAGE_ID_KEY, messageIdHeader[0]);
}
MimetypeRegistry mimeService = (MimetypeRegistry) context.getInitialContext().get(MIMETYPE_SERVICE_KEY);
List<Blob> blobs = new ArrayList<Blob>();
context.put(ATTACHMENTS_KEY, blobs);
// String[] cte = message.getHeader("Content-Transfer-Encoding");
// process content
getAttachmentParts(message, subject, mimeService, context);
context.put(TEXT_KEY, bodyContent);
return true;
} catch (MessagingException | IOException e) {
log.error(e, e);
}
return false;
}
protected static String getFilename(Part p, String defaultFileName) throws MessagingException {
String originalFilename = p.getFileName();
if (originalFilename == null || originalFilename.trim().length() == 0) {
String filename = defaultFileName;
// using default filename => add extension for this type
if (p.isMimeType("text/plain")) {
filename += ".txt";
} else if (p.isMimeType("text/html")) {
filename += ".html";
}
return filename;
} else {
try {
return MimeUtility.decodeText(originalFilename.trim());
} catch (UnsupportedEncodingException e) {
return originalFilename.trim();
}
}
}
protected void getAttachmentParts(Part part, String defaultFilename, MimetypeRegistry mimeService,
ExecutionContext context) throws MessagingException, IOException {
String filename = getFilename(part, defaultFilename);
List<Blob> blobs = (List<Blob>) context.get(ATTACHMENTS_KEY);
if (part.isMimeType("multipart/alternative")) {
bodyContent += getText(part);
} else {
if (!part.isMimeType("multipart/*")) {
String disp = part.getDisposition();
// no disposition => mail body, which can be also blob (image for
// instance)
if (disp == null && // convert only text
part.getContentType().toLowerCase().startsWith("text/")) {
bodyContent += decodeMailBody(part);
} else {
Blob blob;
try (InputStream in = part.getInputStream()) {
blob = Blobs.createBlob(in);
}
String mime = DEFAULT_BINARY_MIMETYPE;
try {
if (mimeService != null) {
ContentType contentType = new ContentType(part.getContentType());
mime = mimeService.getMimetypeFromFilenameAndBlobWithDefault(filename, blob,
contentType.getBaseType());
}
} catch (MessagingException | MimetypeDetectionException e) {
log.error(e);
}
blob.setMimeType(mime);
blob.setFilename(filename);
blobs.add(blob);
}
}
if (part.isMimeType("multipart/*")) {
// This is a Multipart
Multipart mp = (Multipart) part.getContent();
int count = mp.getCount();
for (int i = 0; i < count; i++) {
getAttachmentParts(mp.getBodyPart(i), defaultFilename, mimeService, context);
}
} else if (part.isMimeType(MESSAGE_RFC822_MIMETYPE)) {
// This is a Nested Message
getAttachmentParts((Part) part.getContent(), defaultFilename, mimeService, context);
}
}
}
/**
* Return the primary text content of the message.
*/
private String getText(Part p) throws MessagingException, IOException {
if (p.isMimeType("text/*")) {
return decodeMailBody(p);
}
if (p.isMimeType("multipart/alternative")) {
// prefer html text over plain text
Multipart mp = (Multipart) p.getContent();
String text = null;
for (int i = 0; i < mp.getCount(); i++) {
Part bp = mp.getBodyPart(i);
if (bp.isMimeType("text/plain")) {
if (text == null) {
text = getText(bp);
}
continue;
} else if (bp.isMimeType("text/html")) {
String s = getText(bp);
if (s != null) {
return s;
}
} else {
return getText(bp);
}
}
return text;
} else if (p.isMimeType("multipart/*")) {
Multipart mp = (Multipart) p.getContent();
for (int i = 0; i < mp.getCount(); i++) {
String s = getText(mp.getBodyPart(i));
if (s != null) {
return s;
}
}
}
return null;
}
/**
* Interprets the body accordingly to the charset used. It relies on the content type being
* ****;charset={charset};******
*
* @return the decoded String
*/
protected static String decodeMailBody(Part part) throws MessagingException, IOException {
String encoding = null;
// try to get encoding from header rather than from Stream !
// unfortunately, this does not seem to be reliable ...
/*
* String[] cteHeader = part.getHeader("Content-Transfer-Encoding"); if (cteHeader!=null && cteHeader.length>0)
* { encoding = cteHeader[0].toLowerCase(); }
*/
// fall back to default sniffing
// that will actually read the stream from server
if (encoding == null) {
encoding = MimeUtility.getEncoding(part.getDataHandler());
}
InputStream is = null;
try {
is = MimeUtility.decode(part.getInputStream(), encoding);
} catch (IOException ex) {
log.error("Unable to read content", ex);
return "";
}
String contType = part.getContentType();
final String charsetIdentifier = "charset=";
final String ISO88591 = "iso-8859-1";
final String WINDOWS1252 = "windows-1252";
int offset = contType.indexOf(charsetIdentifier);
String charset = "";
if (offset >= 0) {
charset = contType.substring(offset + charsetIdentifier.length());
offset = charset.indexOf(";");
if (offset > 0) {
charset = charset.substring(0, offset);
}
}
// Charset could be like "utf-8" or utf-8
if (!"".equals(charset)) {
charset = charset.replaceAll("\"", "");
}
log.debug("Content type: " + contType + "; charset: " + charset);
if (charset.equalsIgnoreCase(ISO88591)) {
// see
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#character1
// for more details see http://en.wikipedia.org/wiki/ISO_8859-1
// section "ISO-8859-1 and Windows-1252 confusion"
charset = WINDOWS1252;
log.debug("Using replacing charset: " + charset);
}
String ret;
byte[] streamContent = IOUtils.toByteArray(is);
if ("".equals(charset)) {
ret = new String(streamContent);
} else {
try {
ret = new String(streamContent, charset);
} catch (UnsupportedEncodingException e) {
// try without encoding
ret = new String(streamContent);
}
}
return ret;
}
public Collection<String> getHeaderValues(Message message, String headerName) throws MessagingException {
Collection<String> valuesList = new ArrayList<String>();
String[] values = message.getHeader(headerName);
if (values != null) {
for (String value : values) {
valuesList.add(value);
}
}
return valuesList;
}
}