org.orbeon.oxf.processor.EmailProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.orbeon.oxf.processor.EmailProcessor.java

Source

/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.Element;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.processor.generator.URLGenerator;
import org.orbeon.oxf.processor.serializer.BinaryTextXMLReceiver;
import org.orbeon.oxf.resources.URLFactory;
import org.orbeon.oxf.util.*;
import org.orbeon.oxf.xml.*;
import org.orbeon.oxf.xml.dom4j.*;
import org.xml.sax.*;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.util.Iterator;
import java.util.Properties;

/**
 * This processor allows sending emails. It supports multipart messages and inline as well as out-of-line attachments.
 *
 * For some useful JavaMail information: http://java.sun.com/products/javamail/FAQ.html
 *
 * TODO:
 *
 * o revise support of text/html
 * o built-in support for HTML could handle src="cid:*" with part/message ids
 * o support text/xml? or just XHTML?
 * o build message with SAX, not DOM, so streaming of input is possible [not necessarily a big win]
 */
public class EmailProcessor extends ProcessorImpl {

    private static final Logger logger = LoggerFactory.createLogger(EmailProcessor.class);

    // Properties for this processor
    public static final String EMAIL_SMTP_HOST = "smtp-host";
    public static final String EMAIL_TEST_TO = "test-to";
    public static final String EMAIL_TEST_SMTP_HOST = "test-smtp-host";

    public static final String EMAIL_FORCE_TO_DEPRECATED = "forceto"; // deprecated
    public static final String EMAIL_HOST_DEPRECATED = "host"; // deprecated

    public static final String EMAIL_CONFIG_NAMESPACE_URI = "http://www.orbeon.com/oxf/email";

    private static final String DEFAULT_MULTIPART = "mixed";

    // Use utf-8 as most email clients support it. This allows us not to have to pick an inferior encoding.
    private static final String DEFAULT_CHARACTER_ENCODING = "utf-8";

    public EmailProcessor() {
        addInputInfo(new ProcessorInputOutputInfo(INPUT_DATA, EMAIL_CONFIG_NAMESPACE_URI));
    }

    public void start(PipelineContext pipelineContext) {
        try {
            final Document dataDocument = readInputAsDOM4J(pipelineContext, INPUT_DATA);
            final Element messageElement = dataDocument.getRootElement();

            // Get system id (will likely be null if document is generated dynamically)
            final LocationData locationData = (LocationData) messageElement.getData();
            final String dataInputSystemId = locationData.getSystemID();

            // Set SMTP host
            final Properties properties = new Properties();
            final String testSmtpHostProperty = getPropertySet().getString(EMAIL_TEST_SMTP_HOST);

            if (testSmtpHostProperty != null) {
                // Test SMTP Host from properties overrides the local configuration
                properties.setProperty("mail.smtp.host", testSmtpHostProperty);
            } else {
                // Try regular config parameter and property
                String host = messageElement.element("smtp-host").getTextTrim();
                if (host != null && !host.equals("")) {
                    // Precedence goes to the local config parameter
                    properties.setProperty("mail.smtp.host", host);
                } else {
                    // Otherwise try to use a property
                    host = getPropertySet().getString(EMAIL_SMTP_HOST);
                    if (host == null)
                        host = getPropertySet().getString(EMAIL_HOST_DEPRECATED);
                    if (host == null)
                        throw new OXFException("Could not find SMTP host in configuration or in properties");
                    properties.setProperty("mail.smtp.host", host);

                }
            }

            // Create session
            final Session session;
            {
                // Get credentials
                final String usernameTrimmed;
                final String passwordTrimmed;
                {
                    final Element credentials = messageElement.element("credentials");
                    if (credentials != null) {
                        final Element usernameElement = credentials.element("username");
                        final Element passwordElement = credentials.element("password");
                        usernameTrimmed = (usernameElement != null) ? usernameElement.getStringValue().trim()
                                : null;
                        passwordTrimmed = (passwordElement != null) ? passwordElement.getStringValue().trim() : "";
                    } else {
                        usernameTrimmed = null;
                        passwordTrimmed = null;
                    }
                }

                // Check if credentials are supplied
                if (StringUtils.isNotEmpty(usernameTrimmed)) {
                    // NOTE: A blank username doesn't trigger authentication

                    if (logger.isInfoEnabled())
                        logger.info("Authentication");
                    // Set the auth property to true
                    properties.setProperty("mail.smtp.auth", "true");

                    if (logger.isInfoEnabled())
                        logger.info("Username: " + usernameTrimmed);

                    // Create an authenticator
                    final Authenticator authenticator = new SMTPAuthenticator(usernameTrimmed, passwordTrimmed);

                    // Create session with authenticator
                    session = Session.getInstance(properties, authenticator);
                } else {
                    if (logger.isInfoEnabled())
                        logger.info("No Authentication");
                    session = Session.getInstance(properties);
                }
            }

            // Create message
            final Message message = new MimeMessage(session);

            // Set From
            message.addFrom(createAddresses(messageElement.element("from")));

            // Set To
            String testToProperty = getPropertySet().getString(EMAIL_TEST_TO);
            if (testToProperty == null)
                testToProperty = getPropertySet().getString(EMAIL_FORCE_TO_DEPRECATED);

            if (testToProperty != null) {
                // Test To from properties overrides local configuration
                message.addRecipient(Message.RecipientType.TO, new InternetAddress(testToProperty));
            } else {
                // Regular list of To elements
                for (final Element toElement : Dom4jUtils.elements(messageElement, "to")) {
                    final InternetAddress[] addresses = createAddresses(toElement);
                    message.addRecipients(Message.RecipientType.TO, addresses);
                }
            }

            // Set Cc
            for (final Element ccElement : Dom4jUtils.elements(messageElement, "cc")) {
                final InternetAddress[] addresses = createAddresses(ccElement);
                message.addRecipients(Message.RecipientType.CC, addresses);
            }

            // Set Bcc
            for (final Element bccElement : Dom4jUtils.elements(messageElement, "bcc")) {
                final InternetAddress[] addresses = createAddresses(bccElement);
                message.addRecipients(Message.RecipientType.BCC, addresses);
            }

            // Set headers if any
            for (final Element headerElement : Dom4jUtils.elements(messageElement, "header")) {
                final String headerName = headerElement.element("name").getTextTrim();
                final String headerValue = headerElement.element("value").getTextTrim();

                // NOTE: Use encodeText() in case there are non-ASCII characters
                message.addHeader(headerName,
                        MimeUtility.encodeText(headerValue, DEFAULT_CHARACTER_ENCODING, null));
            }

            // Set the email subject
            // The JavaMail spec is badly written and is not clear about whether this needs to be done here. But it
            // seems to use the platform's default charset, which we don't want to deal with. So we preemptively encode.
            // The result is pure ASCII so that setSubject() will not attempt to re-encode it.
            message.setSubject(MimeUtility.encodeText(messageElement.element("subject").getStringValue(),
                    DEFAULT_CHARACTER_ENCODING, null));

            // Handle body
            final Element textElement = messageElement.element("text");
            final Element bodyElement = messageElement.element("body");

            if (textElement != null) {
                // Old deprecated mechanism (simple text body)
                message.setText(textElement.getStringValue());
            } else if (bodyElement != null) {
                // New mechanism with body and parts
                handleBody(pipelineContext, dataInputSystemId, message, bodyElement);
            } else {
                throw new OXFException("Main text or body element not found");// TODO: location info
            }

            // Send message
            final Transport transport = session.getTransport("smtp");
            Transport.send(message);
            transport.close();
        } catch (Exception e) {
            throw new OXFException(e);
        }
    }

    private void handleBody(PipelineContext pipelineContext, String dataInputSystemId, Part parentPart,
            Element bodyElement) throws Exception {

        // Find out if there are embedded parts
        final Iterator parts = bodyElement.elementIterator("part");
        String multipart;
        if (bodyElement.getName().equals("body")) {
            multipart = bodyElement.attributeValue("mime-multipart");
            if (multipart != null && !parts.hasNext())
                throw new OXFException("mime-multipart attribute on body element requires part children elements");
            // TODO: Check following lines, which were doing nothing!
            //            final String contentTypeFromAttribute = NetUtils.getContentTypeMediaType(bodyElement.attributeValue("content-type"));
            //            if (contentTypeFromAttribute != null && contentTypeFromAttribute.startsWith("multipart/"))
            //                contentTypeFromAttribute.substring("multipart/".length());
            if (parts.hasNext() && multipart == null)
                multipart = DEFAULT_MULTIPART;
        } else {
            final String contentTypeAttribute = NetUtils
                    .getContentTypeMediaType(bodyElement.attributeValue("content-type"));
            multipart = (contentTypeAttribute != null && contentTypeAttribute.startsWith("multipart/"))
                    ? contentTypeAttribute.substring("multipart/".length())
                    : null;
        }

        if (multipart != null) {
            // Multipart content is requested
            final MimeMultipart mimeMultipart = new MimeMultipart(multipart);

            // Iterate through parts
            while (parts.hasNext()) {
                final Element partElement = (Element) parts.next();

                final MimeBodyPart mimeBodyPart = new MimeBodyPart();
                handleBody(pipelineContext, dataInputSystemId, mimeBodyPart, partElement);
                mimeMultipart.addBodyPart(mimeBodyPart);
            }

            // Set content on parent part
            parentPart.setContent(mimeMultipart);
        } else {
            // No multipart, just use the content of the element and add to the current part (which can be the main message)
            handlePart(pipelineContext, dataInputSystemId, parentPart, bodyElement);
        }
    }

    private void handlePart(PipelineContext pipelineContext, String dataInputSystemId, Part parentPart,
            Element partOrBodyElement) throws Exception {
        final String name = partOrBodyElement.attributeValue("name");
        String contentTypeAttribute = partOrBodyElement.attributeValue("content-type");
        final String contentType = NetUtils.getContentTypeMediaType(contentTypeAttribute);
        final String charset;
        {
            final String c = NetUtils.getContentTypeCharset(contentTypeAttribute);
            charset = (c != null) ? c : DEFAULT_CHARACTER_ENCODING;
        }
        final String contentTypeWithCharset = contentType + "; charset=" + charset;
        final String src = partOrBodyElement.attributeValue("src");

        // Either a String or a FileItem
        final Object content;
        if (src != null) {
            // Content of the part is not inline

            // Generate a FileItem from the source
            final SAXSource source = getSAXSource(EmailProcessor.this, pipelineContext, src, dataInputSystemId,
                    contentType);
            content = handleStreamedPartContent(pipelineContext, source);
        } else {
            // Content of the part is inline

            // In the cases of text/html and XML, there must be exactly one root element
            final boolean needsRootElement = "text/html".equals(contentType);// || ProcessorUtils.isXMLContentType(contentType);
            if (needsRootElement && partOrBodyElement.elements().size() != 1)
                throw new ValidationException(
                        "The <body> or <part> element must contain exactly one element for text/html",
                        (LocationData) partOrBodyElement.getData());

            // Create Document and convert it into a String
            final Element rootElement = (Element) (needsRootElement ? partOrBodyElement.elements().get(0)
                    : partOrBodyElement);
            final Document partDocument = new NonLazyUserDataDocument();
            partDocument.setRootElement((Element) rootElement.clone());
            content = handleInlinePartContent(partDocument, contentType);
        }

        if (!XMLUtils.isTextOrJSONContentType(contentType)) {
            // This is binary content (including application/xml)
            if (content instanceof FileItem) {
                final FileItem fileItem = (FileItem) content;
                parentPart.setDataHandler(new DataHandler(new DataSource() {
                    public String getContentType() {
                        return contentType;
                    }

                    public InputStream getInputStream() throws IOException {
                        return fileItem.getInputStream();
                    }

                    public String getName() {
                        return name;
                    }

                    public OutputStream getOutputStream() throws IOException {
                        throw new IOException("Write operation not supported");
                    }
                }));
            } else {
                byte[] data = NetUtils.base64StringToByteArray((String) content);
                parentPart.setDataHandler(new DataHandler(new SimpleBinaryDataSource(name, contentType, data)));
            }
        } else {
            // This is text content (including text/xml)
            if (content instanceof FileItem) {
                // The text content was encoded when written to the FileItem
                final FileItem fileItem = (FileItem) content;
                parentPart.setDataHandler(new DataHandler(new DataSource() {
                    public String getContentType() {
                        // This always contains a charset
                        return contentTypeWithCharset;
                    }

                    public InputStream getInputStream() throws IOException {
                        // This is encoded with the appropriate charset (user-defined, or the default)
                        return fileItem.getInputStream();
                    }

                    public String getName() {
                        return name;
                    }

                    public OutputStream getOutputStream() throws IOException {
                        throw new IOException("Write operation not supported");
                    }
                }));
            } else {
                parentPart.setDataHandler(
                        new DataHandler(new SimpleTextDataSource(name, contentTypeWithCharset, (String) content)));
            }
        }

        // Set content-disposition header
        String contentDisposition = partOrBodyElement.attributeValue("content-disposition");
        if (contentDisposition != null)
            parentPart.setDisposition(contentDisposition);

        // Set content-id header
        String contentId = partOrBodyElement.attributeValue("content-id");
        if (contentId != null)
            parentPart.setHeader("content-id", "<" + contentId + ">");
        //part.setContentID(contentId);
    }

    private String handleInlinePartContent(Document document, String contentType) throws SAXException {
        if ("text/html".equals(contentType)) {
            // Convert XHTML into an HTML String
            StringBuilderWriter writer = new StringBuilderWriter();
            TransformerHandler identity = TransformerUtils.getIdentityTransformerHandler();
            identity.getTransformer().setOutputProperty(OutputKeys.METHOD, "html");
            identity.setResult(new StreamResult(writer));
            LocationSAXWriter saxw = new LocationSAXWriter();
            saxw.setContentHandler(identity);
            saxw.write(document);

            return writer.toString();
        } else {
            // For other types, just return the text nodes
            return document.getStringValue();
        }
    }

    /**
     * Read a text or binary document and return it as a FileItem
     *
     * @param pipelineContext current PipelineContext
     * @param source          SAX source
     * @return                FileItem
     * @throws IOException
     * @throws TransformerException
     */
    // NOTE: Method is public for unit tests
    public static FileItem handleStreamedPartContent(PipelineContext pipelineContext, SAXSource source)
            throws IOException, TransformerException {

        final FileItem fileItem = NetUtils.prepareFileItem(NetUtils.REQUEST_SCOPE);
        TransformerUtils.sourceToSAX(source, new BinaryTextXMLReceiver(fileItem.getOutputStream()));

        return fileItem;
    }

    private InternetAddress[] createAddresses(Element addressElement)
            throws AddressException, UnsupportedEncodingException {
        final String email = addressElement.element("email").getStringValue();
        final Element nameElement = addressElement.element("name");
        // If only the <email> element is specified, allow for comma-separated addresses
        return nameElement == null ? InternetAddress.parse(email)
                : new InternetAddress[] { new InternetAddress(email, nameElement.getStringValue()) };
    }

    private class SimpleTextDataSource implements DataSource {
        String contentType;
        String text;
        String name;

        public SimpleTextDataSource(String name, String contentType, String text) {
            this.name = name;
            this.contentType = contentType;
            this.text = text;
        }

        public String getContentType() {
            return contentType;
        }

        public InputStream getInputStream() throws IOException {
            return new ByteArrayInputStream(text.getBytes("utf-8"));
        }

        public String getName() {
            return name;
        }

        public OutputStream getOutputStream() throws IOException {
            throw new IOException("Write operation not supported");
        }
    }

    private class SimpleBinaryDataSource implements DataSource {
        String contentType;
        byte[] data;
        String name;

        public SimpleBinaryDataSource(String name, String contentType, byte[] data) {
            this.name = name;
            this.contentType = contentType;
            this.data = data;
        }

        public String getContentType() {
            return contentType;
        }

        public InputStream getInputStream() throws IOException {
            return new ByteArrayInputStream(data);
        }

        public String getName() {
            return name;
        }

        public OutputStream getOutputStream() throws IOException {
            throw new IOException("Write operation not supported");
        }
    }

    public static SAXSource getSAXSource(Processor processor, PipelineContext pipelineContext, String href,
            String base, String contentType) {
        try {
            // There are two cases:
            // 1. We read the source as SAX
            //    o This is required when reading from a processor input; in this case, we behave like
            //      the inline case
            //    o When reading from another type of URI, the resource could be in theory any type
            //      of file.
            // 2. We don't read the source as SAX
            //    o It is particularly useful to support this when resources are to be used as binary
            //      attachments such as images.
            //    o Here, we consider that the source can be XML, text/html, text/*,
            //      or binary. We do not handle reading Base64-encoded files. We leverage the URL
            //      generator to obtain the content in XML format.
            XMLReader xmlReader;
            {
                String inputName = ProcessorImpl.getProcessorInputSchemeInputName(href);
                if (inputName != null) {
                    // Resolve to input of current processor
                    xmlReader = new ProcessorOutputXMLReader(pipelineContext,
                            processor.getInputByName(inputName).getOutput());
                } else {
                    // Resolve to regular URI
                    Processor urlGenerator = (contentType == null)
                            ? new URLGenerator(URLFactory.createURL(base, href))
                            : new URLGenerator(URLFactory.createURL(base, href), contentType, true);
                    xmlReader = new ProcessorOutputXMLReader(pipelineContext,
                            urlGenerator.createOutput(ProcessorImpl.OUTPUT_DATA));
                }
            }

            // Return SAX Source based on XML Reader
            SAXSource saxSource = new SAXSource(xmlReader, new InputSource());
            saxSource.setSystemId(href);
            return saxSource;
        } catch (IOException e) {
            throw new OXFException(e);
        }
    }

    private class SMTPAuthenticator extends javax.mail.Authenticator {
        private String username;
        private String password;

        public SMTPAuthenticator(String user, String pass) {
            username = user;
            password = pass;
        }

        public PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication(username, password);
        }
    }
}

// Set content-transfer-encoding header
//                    final String contentTransferEncoding = partElement.attributeValue("content-transfer-encoding");
//
//                    MimeBodyPart part = new MimeBodyPart() {
//                        protected void updateHeaders() throws MessagingException {
//                            super.updateHeaders();
//                            if (contentTransferEncoding != null)
//                                setHeader("Content-Transfer-Encoding", contentTransferEncoding);
//                        }
//                    };
//                    // Set content-disposition header
//                    String contentDisposition = partElement.attributeValue("content-disposition");
//                    if (contentDisposition != null)
//                        part.setDisposition(contentDisposition);
//
//                    part.setDataHandler(new DataHandler(new SimpleTextDataSource(name, contentType, content)));
//                    mimeMultipart.addBodyPart(part);