com.cordys.coe.ac.emailio.outbound.EmailMessageFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.cordys.coe.ac.emailio.outbound.EmailMessageFactory.java

Source

/**
* Copyright 2007 Cordys R&D B.V. 
* 
* This file is part of the Cordys Email IO Connector. 
*
* 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.
*/
package com.cordys.coe.ac.emailio.outbound;

import com.cordys.coe.ac.emailio.config.outbound.IEmailAddress;
import com.cordys.coe.ac.emailio.config.outbound.IHeader;
import com.cordys.coe.ac.emailio.config.outbound.IMailData;
import com.cordys.coe.ac.emailio.config.outbound.IMultiPart;
import com.cordys.coe.ac.emailio.config.outbound.ISendMailData;
import com.cordys.coe.ac.emailio.exception.KeyManagerException;
import com.cordys.coe.ac.emailio.exception.OutboundEmailException;
import com.cordys.coe.ac.emailio.keymanager.ICertificateInfo;
import com.cordys.coe.ac.emailio.localization.OutboundEmailExceptionMessages;
import com.cordys.coe.ac.emailio.util.StringUtil;

import com.eibus.util.logger.CordysLogger;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import java.security.PrivateKey;

import java.security.cert.CertStore;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.X509Certificate;

import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;

import javax.activation.DataHandler;

import javax.mail.Address;

import javax.mail.Message.RecipientType;

import javax.mail.MessagingException;
import javax.mail.Session;

import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

import javax.mail.util.ByteArrayDataSource;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.smime.SMIMECapabilitiesAttribute;
import org.bouncycastle.asn1.smime.SMIMECapability;
import org.bouncycastle.asn1.smime.SMIMECapabilityVector;
import org.bouncycastle.asn1.smime.SMIMEEncryptionKeyPreferenceAttribute;
import org.bouncycastle.asn1.x509.X509Name;

import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator;
import org.bouncycastle.mail.smime.SMIMESignedGenerator;

import org.bouncycastle.util.Strings;

/**
 * This factory class is the link between the configuration data and the actual JavaMail API. Based
 * on the configuration it will create the corresponding mime message.
 *
 * @author  pgussow
 */
public class EmailMessageFactory {
    /**
     * Holds the logger to use.
     */
    private static final CordysLogger LOG = CordysLogger.getCordysLogger(EmailMessageFactory.class);

    /**
     * This method creates the actual MimeMessages that should be sent. First of all the
     * MimeBodypart is created. If signing is needed, the signing will take place next. When the
     * signing is done the actual Mail Message is constructed including all headers passed on.
     * Optionally the MimeMessage is encrypted as well.
     *
     * @param   smdData   The send mail data that contains the details for the actual mail.
     * @param   scConfig  The configuration to use for creating the new mail.
     * @param   sSession  The JavaMail API session to use.
     *
     * @return  The list of messages that have to be send to the mail server.
     *
     * @throws  OutboundEmailException  In case of any exceptions.
     */
    public static List<MimeMessage> createProperMimeMessages(ISendMailData smdData, ISMIMEConfiguration scConfig,
            Session sSession) throws OutboundEmailException {
        List<MimeMessage> lmmReturn = new ArrayList<MimeMessage>();

        try {
            // Instead of directly building up the MimeMessage, we'll build up a bodypart. This is
            // easier IF it turns out that signing is required.
            MimeMessage mmFinal = new MimeMessage(sSession);

            if (smdData.hasData()) {
                // The mail has a flat structure with only 1 block of data to send.
                IMailData mdData = smdData.getMailData();

                ByteArrayDataSource bads = new ByteArrayDataSource(mdData.getData(), mdData.getContentType());
                mmFinal.setDataHandler(new DataHandler(bads));

                if (StringUtil.isSet(mdData.getContentDisposition())) {
                    mmFinal.addHeader("Content-Disposition", mdData.getContentDisposition());
                }
            } else {
                // It consists of multiparts, so lets create them
                IMultiPart mpPart = smdData.getMultiPart();

                // Create the multipart to use.
                MimeMultipart mmMultiPart = new MimeMultipart(mpPart.getSubType());

                // Process all nested parts.
                processNestedParts(mmMultiPart, mpPart.getMultiParts());

                // Now add it to the body part.
                mmFinal.setContent(mmMultiPart);
            }

            // Now we need to determine whether or not the mail needs to be signed.
            if (scConfig.getSMIMEEnabled() && smdData.getSendOptions().getSignMail()) {
                mmFinal = signMessage(mmFinal, scConfig, sSession, smdData.getFrom().getEmailAddress());
            }

            // Now we can add the additional headers to the mail and set the from,to, cc and bcc.
            mmFinal.setFrom(smdData.getFrom().getInternetAddress());

            if (smdData.getReplyTo() != null) {
                mmFinal.setReplyTo(new Address[] { smdData.getReplyTo().getInternetAddress() });
            }

            // Set the recipients.
            processRecipients(RecipientType.TO, smdData.getTo(), mmFinal);
            processRecipients(RecipientType.CC, smdData.getCC(), mmFinal);
            processRecipients(RecipientType.BCC, smdData.getBCC(), mmFinal);

            // Do the subject
            mmFinal.setSubject(smdData.getSubject());
            mmFinal.setSentDate(new Date());

            // Do the additional headers. Only the main part has complete freedom according to
            // the standards.
            IHeader[] ahHeaders = smdData.getHeaders();

            for (IHeader hHeader : ahHeaders) {
                mmFinal.addHeader(hHeader.getName(), hHeader.getValue());
            }

            // Make sure the object is stored properly.
            mmFinal.saveChanges();

            // Final step: do the encryption if needed.
            if (scConfig.getSMIMEEnabled() && smdData.getSendOptions().getEncryptMail()) {
                encryptMessage(lmmReturn, mmFinal, scConfig, sSession);
            } else {
                lmmReturn.add(mmFinal);
            }
        } catch (Exception e) {
            throw new OutboundEmailException(e,
                    OutboundEmailExceptionMessages.OEE_ERROR_CREATING_MIME_MESSAGE_FOR_EMAIL_DATA);
        }
        return lmmReturn;
    }

    /**
     * This method encrypt the given email using the public keys for all senders.
     *
     * @param   lMessages         The list to add all individual messages to.
     * @param   mmMessage         The message to encrypt.
     * @param   eicConfiguration  The configuration of the connector.
     * @param   sSession          The JavaMail session to use.
     *
     * @throws  OutboundEmailException  In case of any exceptions.
     */
    private static void encryptMessage(List<MimeMessage> lMessages, MimeMessage mmMessage,
            ISMIMEConfiguration eicConfiguration, Session sSession) throws OutboundEmailException {
        // Create the encrypter object
        SMIMEEnvelopedGenerator encrypter = new SMIMEEnvelopedGenerator();

        try {
            // Add the public keys of all receivers to the encrypter.
            Address[] aaAdresses = mmMessage.getAllRecipients();

            for (Address address : aaAdresses) {
                InternetAddress ia = (InternetAddress) address;

                // Find the public key for the given email address.
                ICertificateInfo ciRecipient = eicConfiguration.getCertificateInfo(ia.getAddress());

                if ((ciRecipient == null) && !eicConfiguration.getBypassSMIME()) {
                    throw new OutboundEmailException(
                            OutboundEmailExceptionMessages.OEE_COULD_NOT_FIND_THE_PUBLIC_KEY_FOR_THE_EMAIL_ADDRESS_0,
                            ia.toString());
                }

                if (ciRecipient != null) {
                    encrypter.addKeyTransRecipient(ciRecipient.getX509Certificate());
                } else {
                    // Now we could have a funny situation. The following might happen: a mail has
                    // to send to 3 recipients. 1 has no certificate and the bypasssmime is enabled.
                    // What to do now? We need to create a new version of the mail in plain text and
                    // remove all recipients except this one. NOTE: We cannot avoid the recipient
                    // getting 2 mails: 1 unreadable version and 1 plain one. This is beacuse we
                    // cannot remove the recipient from the original message because that message
                    // has been signed.
                    if (eicConfiguration.getBypassSMIME()) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug(
                                    "BypassSMIME is enabled, so going to send a plain text version for user " + ia);
                        }

                        MimeMessage mmPlain = new MimeMessage(mmMessage);
                        mmPlain.setRecipient(RecipientType.TO, ia);
                        mmPlain.saveChanges();
                        lMessages.add(mmPlain);
                    } else {
                        // We need to throw an error since we cannot encrypt to all users.
                        throw new OutboundEmailException(
                                OutboundEmailExceptionMessages.OEE_COULD_NOT_FIND_A_CERTIFICATE_FOR_RECIPIENT_0,
                                ia.toString());
                    }
                }
            }

            // Encrypt the message
            MimeBodyPart encryptedPart = encrypter.generate(mmMessage, SMIMEEnvelopedGenerator.DES_EDE3_CBC, "BC");

            // Create a new MimeMessage that contains the encrypted and signed content
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            encryptedPart.writeTo(out);

            MimeMessage encryptedMessage = new MimeMessage(sSession, new ByteArrayInputStream(out.toByteArray()));

            // Set all original MIME headers in the encrypted message
            Enumeration<?> headers = mmMessage.getAllHeaderLines();

            while (headers.hasMoreElements()) {
                String headerLine = (String) headers.nextElement();

                // Make sure not to override any content-* headers from the original message
                if (!Strings.toLowerCase(headerLine).startsWith("content-")) {
                    encryptedMessage.addHeaderLine(headerLine);
                }
            }

            // Add the encrypted message to the list so that it will be sent.
            lMessages.add(encryptedMessage);
        } catch (Exception e) {
            throw new OutboundEmailException(e, OutboundEmailExceptionMessages.OEE_ERROR_ENCRYPTING_THE_MAIL);
        }
    }

    /**
     * This method parses the given multiparts into MimeBody parts and adds them to the given
     * MimeMultiPart object.
     *
     * @param   mmpParent  The parent MimeMultipart which will hold the body parts.
     * @param   ampParts   The actual parts that need to be created.
     *
     * @throws  MessagingException  In case of any email related exceptions.
     * @throws  IOException         In case of any errors creating the Data Source.
     */
    private static void processNestedParts(MimeMultipart mmpParent, IMultiPart[] ampParts)
            throws MessagingException, IOException {
        for (IMultiPart mpData : ampParts) {
            // Create the body part for this multi part.
            // The multipart can contain either data or again a nested multipart.
            MimeBodyPart mbp = new MimeBodyPart();

            if (mpData.hasNestedParts()) {
                // Create the nested multipart which will hold the individual multi-body parts.
                MimeMultipart mmpNested = new MimeMultipart(mpData.getSubType());

                // Process the nested mime parts.
                processNestedParts(mmpNested, mpData.getMultiParts());

                // Set the content of the current body part to the new nested multipart.
                mbp.setContent(mmpNested);
            } else {
                // We have actual content here. So create the proper data source handler for it.
                IMailData mdMailData = mpData.getMailData();
                ByteArrayDataSource bads = new ByteArrayDataSource(mdMailData.getData(),
                        mdMailData.getContentType());
                mbp.setDataHandler(new DataHandler(bads));

                if (StringUtil.isSet(mdMailData.getContentDisposition())) {
                    mbp.addHeader("Content-Disposition", mdMailData.getContentDisposition());
                }
            }

            mmpParent.addBodyPart(mbp);
        }
    }

    /**
     * This method sets the proper recipients.
     *
     * @param   rt           The type for the list.
     * @param   aeAddresses  The list of addresses to add.
     * @param   mmMessage    The message to add the recipients to.
     *
     * @throws  MessagingException  In case of any exceptions.
     */
    private static void processRecipients(RecipientType rt, IEmailAddress[] aeAddresses, MimeMessage mmMessage)
            throws MessagingException {
        if (aeAddresses != null) {
            Address[] aaAddresses = new Address[aeAddresses.length];

            for (int iCount = 0; iCount < aeAddresses.length; iCount++) {
                aaAddresses[iCount] = aeAddresses[iCount].getInternetAddress();
            }
            mmMessage.addRecipients(rt, aaAddresses);
        }
    }

    /**
     * This method creates and returns a signed version of the given mail.
     *
     * @param   mbpToBeSigned     The message to sign.
     * @param   eicConfiguration  The configuration to use.
     * @param   sSession          The main session to use.
     * @param   sSenderAddress    The email address of the sender.
     *
     * @return  The signed message to return.
     *
     * @throws  OutboundEmailException  In case of any exceptions.
     * @throws  KeyManagerException     In case of any key manager related exceptions.
     */
    private static MimeMessage signMessage(MimeMessage mbpToBeSigned, ISMIMEConfiguration eicConfiguration,
            Session sSession, String sSenderAddress) throws OutboundEmailException, KeyManagerException {
        MimeMessage mmReturn = null;

        // Use the address to find the proper private key.
        PrivateKey pkKey = null;
        ICertificateInfo ciInfo = eicConfiguration.getCertificateInfo(sSenderAddress);

        if (ciInfo != null) {
            pkKey = ciInfo.getKey();
        }

        if ((pkKey == null) && !eicConfiguration.getBypassSMIME()) {
            throw new OutboundEmailException(
                    OutboundEmailExceptionMessages.OEE_COULD_NOT_FIND_A_PRIVATE_KEY_FOR_EMAIL_ADDRESS_0,
                    sSenderAddress);
        } else {
            mmReturn = mbpToBeSigned;
        }

        // Create the signed message if possible. If no private key was found and bypassing S/MIME
        // is allowed the original message is returned.
        if (pkKey != null) {
            try {
                // Get the public key.
                X509Certificate xcPublic = ciInfo.getX509Certificate();

                // Create the SMIME capabilities
                SMIMECapabilityVector capabilities = new SMIMECapabilityVector();
                capabilities.addCapability(SMIMECapability.dES_EDE3_CBC);
                capabilities.addCapability(SMIMECapability.rC2_CBC, 128);
                capabilities.addCapability(SMIMECapability.dES_CBC);

                // Create the signing preferences.
                ASN1EncodableVector attributes = new ASN1EncodableVector();
                X509Name name = new X509Name(xcPublic.getIssuerDN().getName());
                IssuerAndSerialNumber issuerAndSerialNumber = new IssuerAndSerialNumber(name,
                        xcPublic.getSerialNumber());
                SMIMEEncryptionKeyPreferenceAttribute encryptionKeyPreferenceAttribute = new SMIMEEncryptionKeyPreferenceAttribute(
                        issuerAndSerialNumber);
                attributes.add(encryptionKeyPreferenceAttribute);
                attributes.add(new SMIMECapabilitiesAttribute(capabilities));

                // Create the signature generator.
                SMIMESignedGenerator signer = new SMIMESignedGenerator();
                signer.addSigner(pkKey, xcPublic,
                        "DSA".equals(pkKey.getAlgorithm()) ? SMIMESignedGenerator.DIGEST_SHA1
                                : SMIMESignedGenerator.DIGEST_MD5,
                        new AttributeTable(attributes), null);

                // Create the list of certificates that will be sent along with the signature. Right
                // now the CA certificate will NOT be sent along with the mail. It is expected that
                // the receiver is capable of verifying the authenticity of the certificate itself.
                List<X509Certificate> certList = new ArrayList<X509Certificate>();
                certList.add(xcPublic);

                CertStore certs = CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList),
                        "BC");
                signer.addCertificatesAndCRLs(certs);

                // Sign the actual message

                // The message that was created will ALWAYS have a multipart. In order to keep it
                // readable in ALL clients we will sign the content of the message, not the whole
                // message.
                MimeMultipart mm = signer.generate(mbpToBeSigned, "BC");
                mmReturn = new MimeMessage(sSession);

                // Set the content of the signed message
                mmReturn.setContent(mm);
                mmReturn.saveChanges();
            } catch (Exception e) {
                throw new OutboundEmailException(e, OutboundEmailExceptionMessages.OEE_ERROR_SIGNING_EMAIL_MESSAGE);
            }
        } else if (LOG.isDebugEnabled()) {
            LOG.debug("Bypassing S/MIME because no private key was found for " + sSenderAddress);
        }

        return mmReturn;
    }
}