mitm.application.djigzo.james.mailets.PDFEncrypt.java Source code

Java tutorial

Introduction

Here is the source code for mitm.application.djigzo.james.mailets.PDFEncrypt.java

Source

/*
 * Copyright (c) 2008-2012, Martijn Brinkers, Djigzo.
 * 
 * This file is part of Djigzo email encryption.
 *
 * Djigzo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License 
 * version 3, 19 November 2007 as published by the Free Software 
 * Foundation.
 *
 * Djigzo 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public 
 * License along with Djigzo. If not, see <http://www.gnu.org/licenses/>
 *
 * Additional permission under GNU AGPL version 3 section 7
 * 
 * If you modify this Program, or any covered work, by linking or 
 * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, 
 * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, 
 * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, 
 * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, 
 * wsdl4j-1.6.1.jar (or modified versions of these libraries), 
 * containing parts covered by the terms of Eclipse Public License, 
 * tyrex license, freemarker license, dom4j license, mx4j license,
 * Spice Software License, Common Development and Distribution License
 * (CDDL), Common Public License (CPL) the licensors of this Program grant 
 * you additional permission to convey the resulting work.
 */
package mitm.application.djigzo.james.mailets;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.activation.DataHandler;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.AddressException;
import javax.mail.internet.MimeMessage;
import javax.mail.util.ByteArrayDataSource;

import mitm.application.djigzo.DjigzoHeader;
import mitm.application.djigzo.User;
import mitm.application.djigzo.UserProperties;
import mitm.application.djigzo.james.DjigzoMailAttributes;
import mitm.application.djigzo.james.DjigzoMailAttributesImpl;
import mitm.application.djigzo.james.MailetUtils;
import mitm.application.djigzo.james.PasswordContainer;
import mitm.application.djigzo.service.SystemServices;
import mitm.application.djigzo.workflow.UserWorkflow;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.mail.HeaderUtils;
import mitm.common.mail.MessageIDCreator;
import mitm.common.mail.MimeMessageWithID;
import mitm.common.mail.matcher.ContentHeaderNameMatcher;
import mitm.common.mail.matcher.HeaderMatcher;
import mitm.common.mail.matcher.NotHeaderNameMatcher;
import mitm.common.pdf.FontProvider;
import mitm.common.pdf.MessagePDFBuilder;
import mitm.common.pdf.OpenPermission;
import mitm.common.pdf.ViewerPreference;
import mitm.common.properties.HierarchicalPropertiesException;
import mitm.common.security.SecurityFactoryFactoryException;
import mitm.common.security.crypto.EncryptorException;
import mitm.common.security.password.PasswordGenerator;
import mitm.common.security.password.PasswordGeneratorImpl;
import mitm.common.util.Check;
import mitm.common.util.URLBuilder;
import mitm.common.util.URLBuilderException;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.james.core.MailImpl;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.PdfEncryptor;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfWriter;

import freemarker.template.SimpleHash;
import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * Mailet that creates and sends new message(s) with the source message converted to PDF, encrypted with a password 
 * and attached to the new message. The PDF password(s) to use are extracted from the mail attributes. The PDF will 
 * be encrypted using AES 128. If the ownerPasswordMode is random a 16 bytes random number will be generated which 
 * is used for the owner password.
 * 
 * Supported mailet parameters:
 * 
 * encryptedProcessor      : the next processor for the PDF encrypted message
 * notEncryptedProcessor   : the next processor if the message could not be encrypted
 * passwordMode            : single or multiple. In 'single' mode one PDF will be created and encrypted with just one 
 *                           password. All recipients will receive the same encrypted PDF. In 'multiple' mode each 
 *                           recipients should have a password which will be used to encrypt a PDF with. In 'multiple' 
 *                           mode mutiple messages will be sent (one for each
 *                           recipient) (default: single)
 * ownerPasswordMode       : sets the password for the owner of the PDF (sameAsUser or random) (default: sameAsUser)
 * viewerPreference        : the viewer preferences of the PDF. See {@link ViewerPreference}  (default: null)
 * templateProperty        : the user property from which the template should be read
 * template                : path to the Freemarker template file (only used if templateProperty cannot be found) 
 * openPermission          : the open permission of the PDF. See {@link OpenPermission}  (default: null)
 * passThrough             : if false the source message will be removed (ghost'ed)
 * maxSubjectLength        : the maximum length of the subject
 * catchRuntimeExceptions  : if true all RunTimeExceptions are caught (default: true)
 * catchErrors             : if true all Errors are caught (default: true)
 * 
 * @author Martijn Brinkers
 *
 */
public class PDFEncrypt extends SenderTemplatePropertySendMail {
    private final static Logger logger = LoggerFactory.getLogger(PDFEncrypt.class);

    /*
     * PDFEncrypt Freemarker template parameter names
     */
    protected static final String REPLY_URL_TEMPLATE_PARAM = "replyURL";
    protected static final String PASSWORD_CONTAINER_TEMPLATE_PARAM = "passwordContainer";
    protected static final String PASSWORD_ID_TEMPLATE_PARAM = "passwordID";

    /*
     * The key under which the ReplySettings will be stored in the activation context
     */
    private final static String REPLY_SETTINGS_ACTIVATION_CONTEXT_KEY = "replySettings";

    /*
     *  Number of bytes we will add to the buffer for the encrypted PDF to try to
     *  prevent buffer growth.
     */
    private final static int ENCRYPTION_OVERHEAD = 4096;

    /*
     * PDF Encryption type and strength.
     */
    private int pdfEncryptionType = PdfWriter.ENCRYPTION_AES_128;

    /*
     * Processor to use for the encrypted pdf message.
     */
    private String encryptedProcessor;

    /*
     * Processor to use for the non encrypted pdf message.
     */
    private String notEncryptedProcessor;

    /*
     * Password mode determines whether a single encrypted pdf will be created or that
     * multiple encrypted pdf's will be created.
     */
    private PasswordMode passwordMode = PasswordMode.SINGLE;

    /*
     * Determines what the owner password should be
     */
    private OwnerPasswordMode ownerPasswordMode = OwnerPasswordMode.SAME_AS_USER;

    /*
     * Number of bytes used for the randomly generated user password. Only applicable with OwnerPasswordMode.RANDOM.
     */
    private int ownerPasswordLength = 16;

    /*
     * Used for generating the owner password. Only applicable with OwnerPasswordMode.RANDOM. 
     */
    private PasswordGenerator passwordGenerator;

    /*
     * The viewer preferences for the pdf document.
     */
    private Set<ViewerPreference> viewerPreferences;

    /*
     * Permissions of the PDF when the PDF is opened by the end user.
     */
    private Set<OpenPermission> openPermissions = new HashSet<OpenPermission>();

    /*
     * Maximum length of the subject for the reply URL. If size exceeds this maximum size the subject 
     * will be truncated.
     * 
     */
    private int maxSubjectLength = 80;

    /*
     * If true the original Message-ID will be used for the encrypted message.
     * 
     * Note: be very carefull when retaining the message-id. Every message should have a unique message-id
     * See https://jira.djigzo.com/browse/GATEWAY-38
     */
    private boolean retainMessageID = false;

    /*
     * If true and a reply-to header is available and valid, the reply to the PDF will be
     * sent to the reply-to 
     */
    private boolean useReplyTo = true;

    /*
     * Service that provides extra fonts to use with the PDF
     */
    private FontProvider fontProvider;

    /*
     * The mailet initialization parameters used by this mailet.
     */
    private enum Parameter {
        ENCRYPTED_PROCESSOR("encryptedProcessor"), NOT_ENCRYPTED_PROCESSOR("notEncryptedProcessor"), PASSWORD_MODE(
                "passwordMode"), OWNER_PASSWORD_MODE("ownerPasswordMode"), VIEWER_PREFERENCE(
                        "viewerPreference"), OPEN_PERMISSION("openPermission"), MAX_SUBJECT_LENGTH(
                                "maxSubjectLength"), RETAIN_MESSAGE_ID(
                                        "retainMessageID"), USE_REPLY_TO("useReplyTo");

        private String name;

        private Parameter(String name) {
            this.name = name;
        }
    };

    private enum PasswordMode {
        SINGLE("single"), MULTIPLE("multiple");

        private String name;

        private PasswordMode(String name) {
            this.name = name;
        }

        public static PasswordMode fromName(String name) {
            for (PasswordMode mode : PasswordMode.values()) {
                if (mode.name.equalsIgnoreCase(name)) {
                    return mode;
                }
            }

            return null;
        }
    };

    private enum OwnerPasswordMode {
        SAME_AS_USER("sameAsUser"), RANDOM("random");

        private String name;

        private OwnerPasswordMode(String name) {
            this.name = name;
        }

        public static OwnerPasswordMode fromName(String name) {
            for (OwnerPasswordMode mode : OwnerPasswordMode.values()) {
                if (mode.name.equalsIgnoreCase(name)) {
                    return mode;
                }
            }

            return null;
        }
    };

    /*
     * Helper class for storing some settings in the activation context
     */
    private static class ReplySettings {
        /*
         * The subject of the reply (in parameter)
         */
        private final String subject;

        /*
         * The recipients of the reply (in parameter)
         */
        private final Collection<MailAddress> recipients;

        /*
         * The reply URL (out parameter)
         */
        private String replyURL;

        /*
         * The reply-to of the source email. Null if no reply-to header was set.
         */
        private String replyTo;

        public ReplySettings(String subject, Collection<MailAddress> recipients, String replyTo) {
            this.subject = subject;
            this.recipients = recipients;
            this.replyTo = replyTo;
        }

        public String getReplyURL() {
            return replyURL;
        }

        public void setReplyURL(String replyURL) {
            this.replyURL = replyURL;
        }

        public String getSubject() {
            return subject;
        }

        public Collection<MailAddress> getRecipients() {
            return recipients;
        }

        public String getReplyTo() {
            return replyTo;
        }
    }

    @Override
    protected Logger getLogger() {
        return logger;
    }

    private void initOpenPermissions() {
        String param = StringUtils.trimToNull(getInitParameter(Parameter.OPEN_PERMISSION.name));

        if (param != null) {
            String[] permissions = StringUtils.split(param, ',');

            for (String permissionName : permissions) {
                permissionName = StringUtils.trim(permissionName);

                OpenPermission openPermission = OpenPermission.fromName(permissionName);

                if (openPermission == null) {
                    throw new IllegalArgumentException(permissionName + " is not a valid OpenPermission.");
                }

                openPermissions.add(openPermission);
            }
        }

        /*
         * Set default permissions to all if nothing was set.
         */
        if (openPermissions.size() == 0) {
            openPermissions.add(OpenPermission.ALL);
        }
    }

    private void initViewerPreferences() {
        String param = StringUtils.trimToNull(getInitParameter(Parameter.VIEWER_PREFERENCE.name));

        if (param != null) {
            String[] preferences = StringUtils.split(param, ',');

            for (String preferenceName : preferences) {
                preferenceName = StringUtils.trim(preferenceName);

                ViewerPreference viewerPreference = ViewerPreference.fromName(preferenceName);

                if (viewerPreference == null) {
                    throw new IllegalArgumentException(preferenceName + " is not a valid ViewerPreference.");
                }

                if (viewerPreferences == null) {
                    viewerPreferences = new HashSet<ViewerPreference>();
                }

                viewerPreferences.add(viewerPreference);
            }
        }
    }

    private void initPasswordMode() {
        String param = getInitParameter(Parameter.PASSWORD_MODE.name);

        if (param != null) {
            passwordMode = PasswordMode.fromName(param);

            if (passwordMode == null) {
                throw new IllegalArgumentException(param + " is not a valid PasswordMode.");
            }
        }
    }

    private void initOwnerPasswordMode() {
        String param = getInitParameter(Parameter.OWNER_PASSWORD_MODE.name);

        if (param != null) {
            ownerPasswordMode = OwnerPasswordMode.fromName(param);

            if (ownerPasswordMode == null) {
                throw new IllegalArgumentException(param + " is not a valid OwnerPasswordMode.");
            }
        }
    }

    private void initPasswordGenerator() {
        try {
            passwordGenerator = new PasswordGeneratorImpl();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("passwordGenerator could not be initialized.", e);
        } catch (NoSuchProviderException e) {
            throw new IllegalStateException("passwordGenerator could not be initialized.", e);
        } catch (SecurityFactoryFactoryException e) {
            throw new IllegalStateException("passwordGenerator could not be initialized.", e);
        }
    }

    private void initRetainMessageID() {
        retainMessageID = getBooleanInitParameter(Parameter.RETAIN_MESSAGE_ID.name, retainMessageID);
    }

    private void initUseReplyTo() {
        useReplyTo = getBooleanInitParameter(Parameter.USE_REPLY_TO.name, useReplyTo);
    }

    @Override
    public void initMailet() throws MessagingException {
        super.initMailet();

        fontProvider = SystemServices.getFontProvider();

        encryptedProcessor = getInitParameter(Parameter.ENCRYPTED_PROCESSOR.name);

        if (encryptedProcessor == null) {
            throw new IllegalArgumentException("encryptedProcessor must be specified.");
        }

        notEncryptedProcessor = getInitParameter(Parameter.NOT_ENCRYPTED_PROCESSOR.name);

        if (notEncryptedProcessor == null) {
            throw new IllegalArgumentException("notEncryptedProcessor must be specified.");
        }

        String param = getInitParameter(Parameter.MAX_SUBJECT_LENGTH.name);

        if (param != null) {
            maxSubjectLength = NumberUtils.toInt(param, maxSubjectLength);
        }

        initPasswordGenerator();
        initPasswordMode();
        initOwnerPasswordMode();
        initOpenPermissions();
        initViewerPreferences();
        initRetainMessageID();
        initUseReplyTo();

        StrBuilder sb = new StrBuilder();

        sb.append("encryptedProcessor: ");
        sb.append(encryptedProcessor);
        sb.append("; ");
        sb.append("notEncryptedProcessor: ");
        sb.append(notEncryptedProcessor);
        sb.append("; ");
        sb.append("passwordMode: ");
        sb.append(passwordMode);
        sb.append("; ");
        sb.append("ownerPasswordMode: ");
        sb.append(ownerPasswordMode);
        sb.append("; ");
        sb.append("openPermissions: ");
        sb.append(openPermissions);
        sb.append("; ");
        sb.append("viewerPreferences: ");
        sb.append(viewerPreferences);
        sb.append("; ");
        sb.append("retainMessageID: ");
        sb.append(retainMessageID);
        sb.append("; ");
        sb.append("useReplyTo: ");
        sb.append(useReplyTo);

        getLogger().info(sb.toString());
    }

    /*
     * Called with the originating user
     * 
     * @see mitm.application.djigzo.james.mailets.SenderTemplatePropertySendMail#onHandleUserEvent(mitm.application.djigzo.User)
     */
    @Override
    protected Template getTemplateFromUser(User user, SimpleHash root)
            throws MessagingException, HierarchicalPropertiesException, IOException {
        /*
         * Single password mode does not support PDF reply because with Single password mode only one PDF will be sent
         * to multiple recipients.
         */
        if (passwordMode == PasswordMode.MULTIPLE) {
            try {
                UserProperties properties = user.getUserPreferences().getProperties();

                String baseReplyURL = properties.getPdfReplyURL();
                String serverSecret = properties.getServerSecret();

                boolean senderAllowed = properties.isPdfReplyAllowed();

                ReplySettings replySettings = getActivationContext().get(REPLY_SETTINGS_ACTIVATION_CONTEXT_KEY,
                        ReplySettings.class);

                if (replySettings != null) {
                    if (senderAllowed) {
                        /*
                         * Use the Reply-To of the message if available
                         */
                        String replyTo = null;

                        if (useReplyTo) {
                            replyTo = replySettings.getReplyTo();
                        }

                        /*
                         * Fallback to the user's email address (i.e., the from) if the Reply-To is not
                         * available or not valid. 
                         */
                        if (StringUtils.isEmpty(replyTo)) {
                            replyTo = user.getEmail();
                        }

                        String replyURL = createReplyURL(baseReplyURL, replySettings.getSubject(),
                                replySettings.getRecipients(), user.getEmail(), replyTo, serverSecret);

                        if (replyURL != null) {
                            replySettings.setReplyURL(replyURL);

                            /*
                             * Set the replyURL in the Freemarker conext so it can be used in the
                             * PDF encryption message templates
                             */
                            root.put(REPLY_URL_TEMPLATE_PARAM, replyURL);
                        }
                    } else {
                        getLogger().debug("Sender {} does not allow reply to PDF.", user.getEmail());
                    }
                } else {
                    getLogger().warn("ReplySettings are not set");
                }
            } catch (HierarchicalPropertiesException e) {
                throw new MessagingException("Error getting reply pdf properties", e);
            }
        }

        return super.getTemplateFromUser(user, root);
    }

    /*
     * Returns the pdf password embedded in the mail attributes. Null if password is not available.
     */
    private PasswordContainer getPasswordContainer(Mail mail) throws EncryptorException {
        DjigzoMailAttributes mailAttributes = new DjigzoMailAttributesImpl(mail);

        byte[] encryptedPassword = mailAttributes.getEncryptedPassword();
        String passwordID = mailAttributes.getPasswordID();

        PasswordContainer passwordContainer = null;

        if (encryptedPassword != null) {
            passwordContainer = new PasswordContainer(encryptedPassword, passwordID);
        }

        return passwordContainer;
    }

    /*
     * Returns the pdf passwords embedded in the mail attributes. Null if passwords are not available.
     */
    private Map<String, PasswordContainer> getPasswords(Mail mail) {
        DjigzoMailAttributes mailAttributes = new DjigzoMailAttributesImpl(mail);

        return mailAttributes.getPasswords();
    }

    private void addEncryptedPDF(MimeMessage message, byte[] pdf) throws MessagingException {
        /*
         * Find the existing PDF. The expect that the message is a multipart/mixed. 
         */
        if (!message.isMimeType("multipart/mixed")) {
            throw new MessagingException("Content-type should have been multipart/mixed.");
        }

        Multipart mp;

        try {
            mp = (Multipart) message.getContent();
        } catch (IOException e) {
            throw new MessagingException("Error getting message content.", e);
        }

        BodyPart pdfPart = null;

        /*
         * Fallback in case the template does not contain a DjigzoHeader.MARKER
         */
        BodyPart fallbackPart = null;

        for (int i = 0; i < mp.getCount(); i++) {
            BodyPart part = mp.getBodyPart(i);

            if (ArrayUtils.contains(part.getHeader(DjigzoHeader.MARKER), DjigzoHeader.ATTACHMENT_MARKER_VALUE)) {
                pdfPart = part;

                break;
            }

            /*
             * Fallback scanning for application/pdf in case the template does not contain a DjigzoHeader.MARKER
             */
            if (part.isMimeType("application/pdf")) {
                fallbackPart = part;
            }
        }

        if (pdfPart == null) {
            if (fallbackPart != null) {
                getLogger().info("Marker not found. Using ocet-stream instead.");

                /*
                 * Use the octet-stream part
                 */
                pdfPart = fallbackPart;
            } else {
                throw new MessagingException("Unable to find the attachment part in the template.");
            }
        }

        pdfPart.setDataHandler(new DataHandler(new ByteArrayDataSource(pdf, "application/pdf")));
    }

    private int getOpenPermissionsIntValue() {
        int intValue = 0;

        for (OpenPermission permission : openPermissions) {
            intValue = intValue | permission.intValue();
        }

        return intValue;
    }

    private byte[] encryptPDF(byte[] unencryptedPDF, String userPassword, String ownerPassword)
            throws IOException, DocumentException {
        PdfReader pdfReader = new PdfReader(unencryptedPDF);

        ByteArrayOutputStream encryptedPDF = new ByteArrayOutputStream(unencryptedPDF.length + ENCRYPTION_OVERHEAD);

        PdfEncryptor.encrypt(pdfReader, encryptedPDF, pdfEncryptionType, userPassword, ownerPassword,
                getOpenPermissionsIntValue());

        return encryptedPDF.toByteArray();
    }

    private String getOwnerPassword(String userPassword) {
        switch (ownerPasswordMode) {
        case SAME_AS_USER:
            return userPassword;
        case RANDOM:
            return passwordGenerator.generatePassword(ownerPasswordLength);
        default:
            throw new IllegalArgumentException("Unknown OwnerPasswordMode.");
        }
    }

    /*
     * Checks if the user with email allows replies to PDF
     */
    private boolean isReplyAllowed(String email) {
        boolean allowed = false;

        try {
            allowed = userWorkflow.getUser(email, UserWorkflow.GetUserMode.CREATE_IF_NOT_EXIST).getUserPreferences()
                    .getProperties().isPdfReplyAllowed();

        } catch (AddressException e) {
            getLogger().error("Error parsing email " + email, e);
        } catch (HierarchicalPropertiesException e) {
            getLogger().error("Error getting isReplyAllowed property for user " + email);
        }

        return allowed;
    }

    private String createReplyURL(String baseURL, String subject, Collection<MailAddress> recipients, String sender,
            String replyTo, String serverSecret) throws MessagingException {
        String replyURL = null;

        if (EmailAddressUtils.INVALID_EMAIL.equals(replyTo) || StringUtils.isEmpty(replyTo)) {
            /*
             * This happens if the sender of the email is an invalid sender. This will normally only happen if
             * the global settings allow PDF reply.
             */
            getLogger().warn("Reply-To of the message is an invalid email address. "
                    + "It's not possible to reply to the PDF.");
        } else if (baseURL != null && serverSecret != null) {
            /*
             * The from of the reply (is equal to the recipient of the encrypted PDF)
             */
            String recipient = null;

            if (recipients.size() == 1) {
                String notYetValidatedRecipient = recipients.iterator().next().toString();

                recipient = EmailAddressUtils.canonicalizeAndValidate(notYetValidatedRecipient, true);

                if (recipient == null) {
                    getLogger().warn("{} is not a valid recipient.", notYetValidatedRecipient);
                }
            } else {
                getLogger().warn("The mail has multiple recipients.  It's not possible to reply to the PDF.");
            }

            if (recipient != null && !isReplyAllowed(recipient)) {
                getLogger().debug("Recipient {} does not allow reply to PDF.", recipient);

                recipient = null;
            }

            if (sender != null && recipient != null) {
                /*
                 * Make sure the subject is not too long
                 */
                subject = StringUtils.abbreviate(StringUtils.trimToEmpty(subject), maxSubjectLength);

                URLBuilder uRLBuilder = SystemServices.getURLBuilder();

                PDFReplyURLBuilder replyBuilder = new PDFReplyURLBuilder(uRLBuilder);

                replyBuilder.setBaseURL(baseURL);
                replyBuilder.setUser(sender);
                replyBuilder.setRecipient(replyTo);
                /*
                 * The from of the reply message is equal to the original recipient of the PDF (i.e, if the
                 * recipient clicks reply, the sender, aka from, of the reply is set to the recipient of the pdf).
                 */
                replyBuilder.setFrom(recipient);
                replyBuilder.setSubject(subject);
                replyBuilder.setKey(serverSecret);

                try {
                    replyURL = replyBuilder.buildURL();
                } catch (URLBuilderException e) {
                    throw new MessagingException("Building reply URL failed.", e);
                }
            }
        }

        return replyURL;
    }

    private MimeMessage createMessage(Mail mail, Collection<MailAddress> recipients,
            PasswordContainer passwordContainer)
            throws MessagingException, MissingRecipientsException, TemplateException, IOException {
        Check.notNull(passwordContainer, "passwordContainer");

        SimpleHash root = new SimpleHash();

        root.put(PASSWORD_CONTAINER_TEMPLATE_PARAM, passwordContainer);
        /*
         * Note: although passwordID can be retrieved from passwordContainer we keep passwordID to make sure 
         * that existing templates that use passwordID are still working
         */
        root.put(PASSWORD_ID_TEMPLATE_PARAM, passwordContainer.getPasswordID());

        /*
         * We should put the real recipient(s) in the Freemarker root. bug https://jira.djigzo.com/browse/GATEWAY-38.
         */
        root.put(RECIPIENTS_TEMPLATE_PARAM, recipients);

        /*
         * Get the Reply-To of the message and validate.
         * 
         * Note: the Reply-To is not canonicalized because that can result in invalid email addresses. For example
         * "email with space"@example.com is only valid with the quotes.
         */
        String replyTo = EmailAddressUtils.validate(EmailAddressUtils.getEmailAddress(
                EmailAddressUtils.getAddress(EmailAddressUtils.getReplyToQuietly(mail.getMessage()))));

        /*
         * The ReplySettings will be placed in the context since they are needed in #getTemplateFromUser to 
         * get the reply URL
         */
        ReplySettings replySettings = new ReplySettings(mail.getMessage().getSubject(), recipients, replyTo);

        getActivationContext().set(REPLY_SETTINGS_ACTIVATION_CONTEXT_KEY, replySettings);

        /*
         * Create a message from a template. 
         * 
         * Note: Calling createMessage will result in a call to #getTemplateFromUser
         */
        MimeMessage containerMessage = createMessage(mail, root);

        /*
         * The call to createMessage should result in the reply URL to be set (if reply is allowed and all
         * required settings are set)
         */
        String replyURL = replySettings.getReplyURL();

        /*
         * Copy all non content headers from source to notificationMessage.
         */
        HeaderMatcher nonContentMatcher = new NotHeaderNameMatcher(new ContentHeaderNameMatcher());

        HeaderUtils.copyHeaders(mail.getMessage(), containerMessage, nonContentMatcher);

        /*
         * Create PDF from the source message
         */
        ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();

        MessagePDFBuilder pdfBuilder = new MessagePDFBuilder();

        pdfBuilder.setFontProvider(fontProvider);

        if (viewerPreferences != null) {
            pdfBuilder.setViewerPreference(viewerPreferences);
        }

        try {
            pdfBuilder.buildPDF(mail.getMessage(), replyURL, pdfStream);
        } catch (DocumentException e) {
            throw new MessagingException("Error building PDF.", e);
        } catch (IOException e) {
            throw new MessagingException("Error building PDF.", e);
        }

        byte[] unencryptedPDF = pdfStream.toByteArray();

        byte[] encryptedPdf;

        try {
            String userPassword = passwordContainer.getPassword();

            String ownerPassword = getOwnerPassword(userPassword);

            encryptedPdf = encryptPDF(unencryptedPDF, userPassword, ownerPassword);
        } catch (IOException e) {
            throw new MessagingException("Error encrypting PDF.", e);
        } catch (DocumentException e) {
            throw new MessagingException("Error encrypting PDF.", e);
        } catch (EncryptorException e) {
            throw new MessagingException("Unable to retrieve password.", e);
        }

        /*
         * Now find and replace the pdf inside the notificationMessage with the encrypted pdf
         */
        addEncryptedPDF(containerMessage, encryptedPdf);

        containerMessage.saveChanges();

        String messageID = null;

        if (retainMessageID) {
            messageID = StringUtils.trimToNull(mail.getMessage().getMessageID());
        }

        if (messageID == null) {
            messageID = MessageIDCreator.getInstance().createUniqueMessageID();
        }

        containerMessage = new MimeMessageWithID(containerMessage, messageID);

        return containerMessage;
    }

    private void sendNewMessage(Mail mail, Collection<MailAddress> recipients, MimeMessage message,
            String processor) throws MessagingException {
        /*
         * We need to create a new Mail object this way to make sure that all attributes are cloned.
         */
        MailImpl newMail = new MailImpl(mail, MailetUtils.createUniqueMailName());

        try {
            newMail.setRecipients(recipients);
            newMail.setMessage(message);
            newMail.setState(processor);

            getMailetContext().sendMail(newMail);
        } finally {
            newMail.dispose();
        }
    }

    private void sendNotification(Mail mail, Collection<MailAddress> recipients,
            PasswordContainer passwordContainer)
            throws MessagingException, MissingRecipientsException, TemplateException, IOException {
        if (passwordContainer != null) {
            MimeMessage message = createMessage(mail, recipients, passwordContainer);

            this.sendNewMessage(mail, recipients, message, encryptedProcessor);
        } else {
            getLogger().warn("PDF Password not found. Sending message unencrypted.");

            this.sendNewMessage(mail, recipients, mail.getMessage(), notEncryptedProcessor);
        }
    }

    /*
     * One message for all recipients will be created
     */
    private void sendNotificationSinglePasswordMode(Mail mail)
            throws MessagingException, MissingRecipientsException, TemplateException, IOException {
        PasswordContainer passwordContainer;
        try {
            passwordContainer = getPasswordContainer(mail);
        } catch (EncryptorException e) {
            throw new MessagingException("Unable to retrieve password.", e);
        }

        sendNotification(mail, getRecipients(mail), passwordContainer);
    }

    /*
     * For each recipient a new message with an encrypted PDF will be created.
     */
    private void sendNotificationMultiplePasswordMode(Mail mail)
            throws MessagingException, MissingRecipientsException, TemplateException, IOException {
        Map<String, PasswordContainer> passwords = getPasswords(mail);

        if (passwords != null) {
            Collection<MailAddress> recipients = getRecipients(mail);

            for (MailAddress recipient : recipients) {
                /*
                 * We will only accept valid email addresses. If an email address is invalid the message 
                 * will not be enrypted
                 */
                String validatedEmail = EmailAddressUtils
                        .canonicalizeAndValidate(recipient.toInternetAddress().getAddress(), true);

                PasswordContainer passwordContainer = (validatedEmail != null ? passwords.get(validatedEmail)
                        : null);

                sendNotification(mail, Collections.singleton(recipient), passwordContainer);
            }
        } else {
            getLogger().warn("PDF passwords not found. Sending message unencrypted.");

            sendNotification(mail, getRecipients(mail), null);
        }
    }

    @Override
    protected void sendMail(Mail mail)
            throws MessagingException, MissingRecipientsException, TemplateException, IOException {
        switch (passwordMode) {
        case SINGLE:
            sendNotificationSinglePasswordMode(mail);
            break;
        case MULTIPLE:
            sendNotificationMultiplePasswordMode(mail);
            break;
        default:
            throw new IllegalArgumentException("Unknown passwordMode.");
        }
    }
}