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

Java tutorial

Introduction

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

Source

/*
 * Copyright (c) 2010-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.IOException;
import java.security.KeyStoreException;
import java.security.cert.CertStoreException;
import java.util.Collections;
import java.util.concurrent.Callable;

import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;

import mitm.application.djigzo.User;
import mitm.application.djigzo.james.MailAddressUtils;
import mitm.application.djigzo.james.MessageOriginatorIdentifier;
import mitm.application.djigzo.james.UserPersister;
import mitm.application.djigzo.service.SystemServices;
import mitm.application.djigzo.workflow.UserWorkflow;
import mitm.common.hibernate.DatabaseActionExecutor;
import mitm.common.hibernate.DatabaseActionExecutorBuilder;
import mitm.common.hibernate.SessionManager;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.properties.HierarchicalPropertiesException;
import mitm.common.security.ca.CA;
import mitm.common.security.ca.CAException;
import mitm.common.security.ca.CASettings;
import mitm.common.security.ca.CASettingsProvider;
import mitm.common.security.ca.CancelCertificateRequestException;
import mitm.common.security.ca.CertificateRequestStore;
import mitm.common.security.ca.Match;
import mitm.common.security.ca.RequestParameters;
import mitm.common.security.ca.RequestUtils;
import mitm.common.util.Check;
import mitm.common.util.KeyedBarrier;

import org.apache.commons.lang.ObjectUtils.Null;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.commons.lang.time.DateUtils;
import org.apache.mailet.Mail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Requests a certificate for the sender.
 * 
 * @author Martijn Brinkers
 * 
 * Supported mailet parameters:
 * 
 * skipIfAvailable           : If true a certificate won't be requested if the sender already has a signing certificate
 *                             or if the certificate request store already contains a pending request for the sender.
 * addUser                   : If true and a certificate is created, the user will be added
 * certificateRequestHandler : The certificate request handler to use. If not specified the certificate request handler
 *                             specified by the CASettings will be used
 * catchRuntimeExceptions    : If true all RunTimeExceptions are caught
 * catchErrors               : If true all Errors are caught
 */
public class RequestSenderCertificate extends AbstractDjigzoMailet {
    private final static Logger logger = LoggerFactory.getLogger(RequestSenderCertificate.class);

    /*
     * The activation context key
     */
    private final static String ACTIVATION_CONTEXT_KEY = "activationContext";

    /*
     * The number of times a database action should be retried when a ConstraintViolation occurs
     */
    private final static int ACTION_RETRIES = 3;

    /*
     * manages database sessions
     */
    private SessionManager sessionManager;

    /*
     * Used for adding and retrieving users
     */
    private UserWorkflow userWorkflow;

    /*
     * Identifies the 'sender' of the message (default version uses from as the identifier)
     */
    private MessageOriginatorIdentifier messageOriginatorIdentifier;

    /*
     * Used for requesting certificates
     */
    private CA ca;

    /*
     * Store that stores pending certificate requests
     */
    private CertificateRequestStore certificateRequestStore;

    /*
     * Used to get the CA settings
     */
    private CASettingsProvider caSettingsProvider;

    /*
     * Used to execute database actions in a transaction
     */
    private DatabaseActionExecutor actionExecutor;

    /*
     * If true a certificate won't be requested if the sender already has a signing certificate or if the certificate 
     * request store already contains a pending request for the sender.
     */
    private boolean skipIfAvailable = true;

    /*
     * If true and a certificate is created, the user will be added
     */
    private boolean addUser = true;

    /*
     * If true, requests are synchronized using a KeyedBarrier
     */
    private boolean synchronizeRequests = true;

    /*
     * Used for synchronizing certificate requests to make sure that no certificate is requested
     * multiple times when a message is sent by the same user in a short timeframe
     */
    private final KeyedBarrier<InternetAddress, Null> keyedBarrier = new KeyedBarrier<InternetAddress, Null>();

    /*
     * The maximum time (in msec) a request can be blocked by the KeyedBarrier
     */
    private final long keyedBarrierTimeout = DateUtils.MILLIS_PER_SECOND * 60;

    /*
     * The certificate request handler to use. If not specified the certificate request handler
     * specified by the CASettings will be used 
     */
    private String certificateRequestHandler;

    /*
     * used to track user objects.
     */
    private static class ActivationContext {
        private User user;

        public void setUser(User user) {
            this.user = user;
        }

        public User getUser() {
            return user;
        }
    }

    /*
     * The mailet initialization parameters used by this mailet.
     */
    private enum Parameter {
        SKIP_IF_AVAILABLE("skipIfAvailable"), ADD_USER("addUser"), SYNCHRONIZE_REQUESTS(
                "synchronizeRequests"), CERTIFICATE_REQUEST_HANDLER("certificateRequestHandler");

        private String name;

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

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

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

        skipIfAvailable = getBooleanInitParameter(Parameter.SKIP_IF_AVAILABLE.name, skipIfAvailable);

        addUser = getBooleanInitParameter(Parameter.ADD_USER.name, addUser);

        synchronizeRequests = getBooleanInitParameter(Parameter.SYNCHRONIZE_REQUESTS.name, synchronizeRequests);

        certificateRequestHandler = getInitParameter(Parameter.CERTIFICATE_REQUEST_HANDLER.name);

        sessionManager = SystemServices.getSessionManager();

        userWorkflow = SystemServices.getUserWorkflow();

        messageOriginatorIdentifier = SystemServices.getMessageOriginatorIdentifier();

        ca = SystemServices.getCA();

        certificateRequestStore = SystemServices.getCertificateRequestStore();

        caSettingsProvider = SystemServices.getCASettingsProvider();

        actionExecutor = DatabaseActionExecutorBuilder.createDatabaseActionExecutor(sessionManager);

        assert (actionExecutor != null);

        StrBuilder sb = new StrBuilder();

        sb.append("skipIfAvailable: ");
        sb.append(skipIfAvailable);
        sb.append("; ");
        sb.append("addUser: ");
        sb.append(addUser);
        sb.append("; ");
        sb.append("synchronizeRequests: ");
        sb.append(synchronizeRequests);
        sb.append("; ");
        sb.append("certificateRequestHandler: ");
        sb.append(certificateRequestHandler);

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

    private void makeNonPersistentUserPersistent(User user) {
        if (user != null && addUser) {
            UserPersister userPersister = new UserPersister(userWorkflow, sessionManager, actionExecutor);

            userPersister.tryToMakeNonPersistentUsersPersistent(Collections.singleton(user));
        }
    }

    private void requestCertificate(User user) throws MessagingException {
        try {
            CASettings caSettings = caSettingsProvider.getSettings();

            RequestParameters request = RequestUtils.createRequestParameters(user.getEmail(), caSettings);

            /*
             * Override the certificate request handler from the CASettings if this mailet overrides it
             */
            if (StringUtils.isNotEmpty(certificateRequestHandler)) {
                request.setCertificateRequestHandler(certificateRequestHandler);
            }

            ca.requestCertificate(request);
        } catch (HierarchicalPropertiesException e) {
            throw new MessagingException("Error requesting certificate.", e);
        } catch (IOException e) {
            throw new MessagingException("Error requesting certificate.", e);
        } catch (CAException e) {
            throw new MessagingException("Error requesting certificate.", e);
        }
    }

    /*
     * Gets called by MailAddressHandler#handleRecipients to handle the user
     */
    protected void onHandleUserEvent(User user) throws MessagingException {
        try {
            ActivationContext activationContext = getActivationContext().get(ACTIVATION_CONTEXT_KEY,
                    ActivationContext.class);

            Check.notNull(activationContext, "activationContext");

            /*
             * Reset in case there was a retry
             */
            activationContext.setUser(null);

            boolean requestCertificate = true;

            if (skipIfAvailable) {
                if (user.getSigningKeyAndCertificate() != null) {
                    logger.debug("The user " + user.getEmail() + " already has a valid signing certificate.");

                    requestCertificate = false;
                }

                if (requestCertificate
                        && certificateRequestStore.getSizeByEmail(user.getEmail(), Match.EXACT) > 0) {
                    logger.debug("there is already a pending request for user " + user.getEmail());

                    requestCertificate = false;
                }
            }

            if (requestCertificate) {
                requestCertificate(user);

                activationContext.setUser(user);
            }
        } catch (KeyStoreException e) {
            throw new MessagingException("Error getting signing key for user: " + user.getEmail(), e);
        } catch (CertStoreException e) {
            throw new MessagingException("Error getting signing key for user: " + user.getEmail(), e);
        } catch (HierarchicalPropertiesException e) {
            throw new MessagingException("Error getting signing key for user: " + user.getEmail(), e);
        }
    }

    @Override
    public void serviceMail(Mail mail) {
        try {
            final ActivationContext activationContext = new ActivationContext();

            /*
             * Place the activationContext in the context so we can use it in onHandleUserEvent
             */
            getActivationContext().set(ACTIVATION_CONTEXT_KEY, activationContext);

            MailAddressHandler.HandleUserEventHandler eventHandler = new MailAddressHandler.HandleUserEventHandler() {
                @Override
                public void handleUser(User user) throws MessagingException {
                    onHandleUserEvent(user);
                }
            };

            final MailAddressHandler mailAddressHandler = new MailAddressHandler(sessionManager, userWorkflow,
                    actionExecutor, eventHandler, ACTION_RETRIES);

            final InternetAddress originator = messageOriginatorIdentifier.getOriginator(mail);

            /*
             * If the from is an invalid email address invalid@invalid.tld will be returned. We don't want to request
             * a certificate for that address.
             */
            if (originator != null && !EmailAddressUtils.isInvalidDummyAddress(originator)) {
                Callable<Null> callable = new Callable<Null>() {
                    @Override
                    public Null call() throws Exception {
                        mailAddressHandler.handleMailAddresses(
                                MailAddressUtils.fromAddressArrayToMailAddressList(originator));

                        /*
                         * activationContext.getUser() will return null if there was no certificate 
                         * requested for the user
                         */
                        makeNonPersistentUserPersistent(activationContext.getUser());

                        return null;
                    }
                };

                try {
                    if (synchronizeRequests) {
                        /*
                         * Synchronize certificate requests to make sure only one certificate is requested for
                         * a user even if multiple messages are sent by the same user at the same time
                         */
                        keyedBarrier.execute(originator, callable, keyedBarrierTimeout);
                    } else {
                        callable.call();
                    }
                } catch (Exception e) {
                    throw new MessagingException("Error requesting a certificate.", e);
                }
            } else {
                logger.debug(
                        "Message originator is an invalid email address. Certificate request will be skipped.");
            }
        } catch (Exception e) {
            Throwable rootCause = ExceptionUtils.getRootCause(e);

            /*
             * If the MessagingException is caused by a CancelCertificateRequestException, do not log stacktrace 
             */
            if (rootCause instanceof CancelCertificateRequestException) {
                getLogger().warn("Certificate request was canceled. Message: " + rootCause.getMessage());
            } else {
                getLogger().error("Error requesting certificates.", e);
            }
        }
    }
}