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

Java tutorial

Introduction

Here is the source code for mitm.application.djigzo.james.mailets.SMIMEHandler.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.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestOutputStream;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.cert.CertStoreException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.MimeMessage;

import mitm.application.djigzo.EncryptionRecipientSelector;
import mitm.application.djigzo.User;
import mitm.application.djigzo.james.DjigzoMailAttributes;
import mitm.application.djigzo.james.DjigzoMailAttributesImpl;
import mitm.application.djigzo.james.MailAddressUtils;
import mitm.application.djigzo.james.MailAttributesUtils;
import mitm.application.djigzo.james.MailetUtils;
import mitm.application.djigzo.james.SecurityInfoTags;
import mitm.application.djigzo.service.SystemServices;
import mitm.application.djigzo.workflow.KeyAndCertificateWorkflow;
import mitm.application.djigzo.workflow.UserWorkflow;
import mitm.application.djigzo.workflow.UserWorkflow.GetUserMode;
import mitm.common.hibernate.DatabaseAction;
import mitm.common.hibernate.DatabaseActionExecutor;
import mitm.common.hibernate.DatabaseActionExecutorBuilder;
import mitm.common.hibernate.DatabaseException;
import mitm.common.hibernate.SessionManager;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.mail.MailSession;
import mitm.common.mail.MimeMessageWithID;
import mitm.common.mail.SkipHeadersOutputStream;
import mitm.common.properties.HierarchicalPropertiesException;
import mitm.common.security.KeyAndCertStore;
import mitm.common.security.KeyAndCertificate;
import mitm.common.security.PKISecurityServices;
import mitm.common.security.SecurityFactoryFactory;
import mitm.common.security.StaticKeysPKISecurityServices;
import mitm.common.security.certstore.X509CertStoreEntry;
import mitm.common.security.smime.SMIMEInspector;
import mitm.common.security.smime.handler.CertificateCollectionEvent;
import mitm.common.security.smime.handler.RecursiveSMIMEHandler;
import mitm.common.security.smime.handler.SMIMEHandlerException;
import mitm.common.security.smime.handler.SMIMEInfoHandlerImpl;
import mitm.common.util.CollectionUtils;
import mitm.common.util.HexUtils;
import mitm.common.util.ReadableOutputStreamBuffer;
import mitm.common.util.SizeUtils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.io.output.TeeOutputStream;
import org.apache.commons.lang.StringUtils;
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.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Mailet that tries to decrypt an encrypted S/MIME message, gets all certificates from the S/MIME message and adds security
 * info to the headers. The S/MIME handler can handle 'normal' message with attached S/MIME messages.
 * 
 * @author Martijn Brinkers
 *
 */
public class SMIMEHandler extends AbstractDjigzoMailet {
    private final static Logger logger = LoggerFactory.getLogger(SMIMEHandler.class);

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

    /*
     * The mailet initialization parameters used by this mailet.
     */
    private enum Parameter {
        REMOVE_SIGNATURE("removeSignature"), IMPORT_CERTIFICATES("importCertificates"), ADD_INFO(
                "addInfo"), DECRYPT("decrypt"), DECOMPRESS("decompress"), MAX_RECURSION(
                        "maxRecursion"), PROTECTED_HEADER("protectedHeader"), RETAIN_MESSAGE_ID(
                                "retainMessageID"), STRICT("strict"), THRESHOLD("threshold"), SUBJECT_TEMPLATE(
                                        "subjectTemplate"), HANDLED_PROCESSOR("handledProcessor"), STRICT_ATTRIBUTE(
                                                "strictAttribute"), REMOVE_SIGNATURE_ATTRIBUTE(
                                                        "removeSignatureAttribute");

        private String name;

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

    private class CertificateCollectionEventImpl implements CertificateCollectionEvent {
        @Override
        public void certificatesEvent(Collection<? extends X509Certificate> certificates)
                throws CertificateException {
            SMIMEHandler.this.certificatesEvent(certificates);
        }
    }

    /*
     * Is called when a certificate is extracted from the message
     */
    private final CertificateCollectionEvent certificateCollectionEvent = new CertificateCollectionEventImpl();

    /*
     * The maximum search depth for embedded S/MIME attachments.
     */
    private int maxRecursion;

    /*
     * If true and the message is signed the signature will be removed.
     */
    private boolean removeSignature;

    /*
     * If true all certificates attached to signed message will be imported into the certificate store
     */
    private boolean importCertificates;

    /*
     * If true security info will be added to the message headers.
     */
    private boolean addInfo;

    /*
     * If true and the message is encrypted the message will be decrypted
     */
    private boolean decrypt;

    /*
     * If true and the message is compressed the message will be decompressed
     */
    private boolean decompress;

    /*
     * If true, the original Message-ID will be used for the handled message
     */
    private boolean retainMessageID;

    /*
     * If true, the message will only be decrypted for a recipient if the recipient
     * has the correct provate key. In non-strict mode, if the message can be decrypted,
     * the message will be delivered undecrypted to all recipients.
     */
    private boolean strict;

    /*
     * The next processor for messages that were handled
     */
    private String handledProcessor;

    /*
     * Determines which headers will be encrypted and/or signed
     */
    private String[] protectedHeaders = new String[] {};

    /*
     * Provides PKI services (root store, path builder etc.)
     */
    private PKISecurityServices pKISecurityServices;

    /*
     * Is used for importing extracted certificates 
     */
    private KeyAndCertificateWorkflow keyAndCertificateWorkflow;

    /*
     * For getting users
     */
    private UserWorkflow userWorkflow;

    /*
     * Used for the selection of encryption certificates for particular user(s).
     */
    private EncryptionRecipientSelector encryptionRecipientSelector;

    /*
     * For managing database sessions
     */
    private SessionManager sessionManager;

    /*
     * Is used to execute a method within a database transaction
     */
    private DatabaseActionExecutor actionExecutor;

    /*
     * If exceeded, messages are stored in a temp file and not in memory
     */
    private int threshold;

    /*
     * The subject template. The first parameter is replaced with the current subject and the second
     * parameter is replaced with the encryption or signing tag.
     * 
     *  Example subject:
     *  
     *  %1$s %2$s 
     */
    private String subjectTemplate;

    /*
     * The name of the strict Mail Attribute
     */
    private String strictAttribute;

    /*
     * The name of the removeSignature Mail Attribute
     */
    private String removeSignatureAttribute;

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

    public int getThreshold() {
        return threshold;
    }

    public void setThreshold(int threshold) {
        this.threshold = threshold;
    }

    private void initProtectedHeaders() {
        String param = getInitParameter(Parameter.PROTECTED_HEADER.name);

        if (param != null) {
            protectedHeaders = param.split("\\s*,\\s*");
        }
    }

    @Override
    final public void initMailet() throws MessagingException {
        getLogger().info("Initializing mailet: " + getMailetName());

        removeSignature = getBooleanInitParameter(Parameter.REMOVE_SIGNATURE.name, false /* default */);
        importCertificates = getBooleanInitParameter(Parameter.IMPORT_CERTIFICATES.name, true/* default */);
        addInfo = getBooleanInitParameter(Parameter.ADD_INFO.name, true /* default */);
        decrypt = getBooleanInitParameter(Parameter.DECRYPT.name, true /* default */);
        decompress = getBooleanInitParameter(Parameter.DECOMPRESS.name, true /* default */);
        retainMessageID = getBooleanInitParameter(Parameter.RETAIN_MESSAGE_ID.name, true /* default */);
        strict = getBooleanInitParameter(Parameter.STRICT.name, false /* default */);
        maxRecursion = getIntegerInitParameter(Parameter.MAX_RECURSION.name, 32 /* default */);
        threshold = getIntegerInitParameter(Parameter.THRESHOLD.name, SizeUtils.MB * 5 /* default */);
        subjectTemplate = getInitParameter(Parameter.SUBJECT_TEMPLATE.name, "%1$s %2$s" /* default */);

        handledProcessor = getInitParameter(Parameter.HANDLED_PROCESSOR.name);

        if (handledProcessor == null) {
            throw new MessagingException("handledProcessor is missing.");
        }

        strictAttribute = getInitParameter(Parameter.STRICT_ATTRIBUTE.name);
        removeSignatureAttribute = getInitParameter(Parameter.REMOVE_SIGNATURE_ATTRIBUTE.name);

        initProtectedHeaders();

        StrBuilder sb = new StrBuilder();

        sb.append("Remove signature: ").append(removeSignature);
        sb.appendSeparator("; ");
        sb.append("Add info: ").append(addInfo);
        sb.appendSeparator("; ");
        sb.append("Decrypt: ").append(decrypt);
        sb.appendSeparator("; ");
        sb.append("Decompress: ").append(decompress);
        sb.appendSeparator("; ");
        sb.append("Retain Message-ID: ").append(retainMessageID);
        sb.appendSeparator("; ");
        sb.append("Strict: ").append(strict);
        sb.appendSeparator("; ");
        sb.append("handledProcessor: ").append(handledProcessor);
        sb.appendSeparator("; ");
        sb.append("Protected headers: ");
        sb.append(StringUtils.join(protectedHeaders, ","));
        sb.appendSeparator("; ");
        sb.append("Max recursion: ").append(maxRecursion);
        sb.appendSeparator("; ");
        sb.append("threshold: ").append(threshold);
        sb.appendSeparator("; ");
        sb.append("subjectTemplate: ").append(subjectTemplate);
        sb.appendSeparator("; ");
        sb.append("strictAttribute: ").append(strictAttribute);
        sb.appendSeparator("; ");
        sb.append("removeSignatureAttribute: ").append(removeSignatureAttribute);

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

        sessionManager = SystemServices.getSessionManager();

        actionExecutor = DatabaseActionExecutorBuilder.createDatabaseActionExecutor(sessionManager);

        assert (actionExecutor != null);

        pKISecurityServices = SystemServices.getPKISecurityServices();

        keyAndCertificateWorkflow = SystemServices.getKeyAndCertificateWorkflow();

        userWorkflow = SystemServices.getUserWorkflow();

        encryptionRecipientSelector = SystemServices.getEncryptionRecipientSelector();
    }

    private boolean isStrict(Mail mail) {
        return MailAttributesUtils.getBoolean(mail, strictAttribute, this.strict);
    }

    private boolean isRemoveSignatureAttribute(Mail mail) {
        return MailAttributesUtils.getBoolean(mail, removeSignatureAttribute, this.removeSignature);
    }

    private void sendNewMessage(Mail sourceMail, 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(sourceMail, MailetUtils.createUniqueMailName());

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

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

    @Override
    public void serviceMail(final Mail mail) {
        try {
            final MimeMessage sourceMessage = mail.getMessage();

            if (sourceMessage != null) {
                Messages messages = actionExecutor.executeTransaction(new DatabaseAction<Messages>() {
                    @Override
                    public Messages doAction(Session session) throws DatabaseException {
                        Session previousSession = sessionManager.getSession();

                        sessionManager.setSession(session);

                        try {
                            return handleMessageTransacted(mail);
                        } finally {
                            sessionManager.setSession(previousSession);
                        }
                    }
                }, ACTION_RETRIES /* retry on a ConstraintViolationException */);

                if (messages != null) {
                    try {
                        Collection<MimeSource> mimeSources = messages.getMimeSources();

                        /*
                         * Send all handled S/MIME message to the handledProcessor next processor 
                         */
                        for (MimeSource mimeSource : mimeSources) {
                            InputStream mimeInput = mimeSource.getMimeSource().getInputStream();

                            MimeMessage message = retainMessageID
                                    ? new MimeMessageWithID(MailSession.getDefaultSession(), mimeInput,
                                            sourceMessage.getMessageID())
                                    : new MimeMessage(MailSession.getDefaultSession(), mimeInput);

                            sendNewMessage(mail, mimeSource.getRecipients(), message, handledProcessor);
                        }

                        /*
                         * Send email to all the recipients that need the message unchanged
                         */
                        Set<MailAddress> asIsRecipients = messages.getAsIsRecipients();

                        if (CollectionUtils.isEmpty(asIsRecipients)) {
                            /*
                             * We no longer need the original Mail so ghost it
                             */
                            mail.setState(Mail.GHOST);
                        } else {
                            mail.setRecipients(asIsRecipients);
                        }
                    } finally {
                        messages.close();
                    }
                }
            }
        } catch (DatabaseException e) {
            getLogger().error("Error handling the message.", e);
        } catch (MessagingException e) {
            getLogger().error("Error handling the message.", e);
        } catch (IOException e) {
            getLogger().error("Error handling the message.", e);
        }
    }

    private void certificatesEvent(Collection<? extends X509Certificate> certificates) throws CertificateException {
        if (importCertificates) {
            for (X509Certificate certificate : certificates) {
                try {
                    keyAndCertificateWorkflow.addCertificateTransacted(certificate);
                } catch (CertStoreException e) {
                    getLogger().error("Error adding certificate. Skipping this certificate.", e);
                }
            }
        }
    }

    private Messages handleMessageTransacted(Mail mail) throws DatabaseException {
        try {
            return isStrict(mail) ? handleMessageTransactedStrict(mail) : handleMessageTransactedNonStrict(mail);
        } catch (MessagingException e) {
            throw new DatabaseException(e);
        }
    }

    private Messages handleMessageTransactedNonStrict(Mail mail) throws MessagingException {
        MimeMessage sourceMessage = mail.getMessage();

        RecursiveSMIMEHandler recursiveSMIMEHandler = new RecursiveSMIMEHandler(pKISecurityServices);

        recursiveSMIMEHandler.setAddInfo(addInfo);
        recursiveSMIMEHandler.setDecrypt(decrypt);
        recursiveSMIMEHandler.setDecompress(decompress);
        recursiveSMIMEHandler.setMaxRecursion(maxRecursion);
        recursiveSMIMEHandler.setRemoveSignature(isRemoveSignatureAttribute(mail));
        recursiveSMIMEHandler.setProtectedHeaders(protectedHeaders);
        recursiveSMIMEHandler.setCertificatesEventListener(certificateCollectionEvent);
        /*
         * Use a different SMIMEInfoHandler so we can add tags (like [encrypt]) to the subject of the message
         */
        recursiveSMIMEHandler.setSmimeInfoHandler(new ExtSMIMEInfoHandlerImpl(mail));

        MimeMessage handledMessage;

        try {
            handledMessage = recursiveSMIMEHandler.handlePart(sourceMessage);
        } catch (SMIMEHandlerException e) {
            throw new MessagingException("Error handling message.", e);
        }

        Messages messages = new Messages(threshold);

        try {
            List<MailAddress> recipients = MailAddressUtils.getRecipients(mail);

            if (handledMessage != null) {
                messages.addMessage(handledMessage, recipients);
            } else {
                /*
                 * The message was not encrypted, could not be decrypted etc.
                 */
                messages.addAsIsRecipient(recipients);
            }
        } catch (Exception e) {
            /*
             * We need to catch all to make sure messages will always be closed.
             * If not there is a change that temp files won't be deleted.
             */
            messages.close();

            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }

            if (e instanceof MessagingException) {
                throw (MessagingException) e;
            }

            throw new MessagingException("Error handling user", e);
        }

        return messages;
    }

    /*
     * Returns all the private keys that are selected for the user. The private keys are the automatically
     * selected keys (i.e., matching email address and valid), the manually selected and the inherited.
     */
    private Set<KeyAndCertificate> getKeyAndCertificates(User user) throws MessagingException {
        try {
            Set<KeyAndCertificate> keys = new HashSet<KeyAndCertificate>();

            Set<X509Certificate> certificates = new HashSet<X509Certificate>();

            encryptionRecipientSelector.select(Collections.singleton(user), certificates);

            KeyAndCertStore keyAndCertStore = pKISecurityServices.getKeyAndCertStore();

            for (X509Certificate certificate : certificates) {
                X509CertStoreEntry entry = keyAndCertStore.getByCertificate(certificate);

                if (entry != null) {
                    KeyAndCertificate keyAndCertificate = keyAndCertStore.getKeyAndCertificate(entry);

                    if (keyAndCertificate != null && keyAndCertificate.getPrivateKey() != null) {
                        keys.add(keyAndCertificate);
                    }
                }
            }
            return keys;
        } catch (CertStoreException e) {
            throw new MessagingException("Error getting key and certificates for user: " + user.getEmail(), e);
        } catch (KeyStoreException e) {
            throw new MessagingException("Error getting key and certificates for user: " + user.getEmail(), e);
        } catch (HierarchicalPropertiesException e) {
            throw new MessagingException("Error getting key and certificates for user: " + user.getEmail(), e);
        }
    }

    private MimeMessage handleMessageForUser(Mail mail, User user) throws MessagingException {
        MimeMessage handledMessage = null;

        Set<KeyAndCertificate> keys = getKeyAndCertificates(user);

        if (logger.isDebugEnabled()) {
            logger.debug("Nr of keys: " + keys.size());
        }

        RecursiveSMIMEHandler recursiveSMIMEHandler = new RecursiveSMIMEHandler(
                new StaticKeysPKISecurityServices(pKISecurityServices, keys));

        recursiveSMIMEHandler.setAddInfo(addInfo);
        recursiveSMIMEHandler.setDecrypt(decrypt);
        recursiveSMIMEHandler.setDecompress(decompress);
        recursiveSMIMEHandler.setMaxRecursion(maxRecursion);
        recursiveSMIMEHandler.setRemoveSignature(isRemoveSignatureAttribute(mail));
        recursiveSMIMEHandler.setProtectedHeaders(protectedHeaders);
        recursiveSMIMEHandler.setCertificatesEventListener(certificateCollectionEvent);
        /*
         * Use a different SMIMEInfoHandler so we can add tags (like [encrypt]) to the subject of the message
         */
        recursiveSMIMEHandler.setSmimeInfoHandler(new ExtSMIMEInfoHandlerImpl(mail));

        try {
            handledMessage = recursiveSMIMEHandler.handlePart(mail.getMessage());
        } catch (SMIMEHandlerException e) {
            logger.error("Error checking for S/MIME for user " + user.getEmail());
        } catch (MessagingException e) {
            logger.error("Error checking for S/MIME for user " + user.getEmail());
        }

        return handledMessage;
    }

    private Messages handleMessageTransactedStrict(Mail mail) throws MessagingException {
        Messages messages = new Messages(threshold);

        try {
            List<MailAddress> recipients = MailAddressUtils.getRecipients(mail);

            for (MailAddress recipient : recipients) {
                if (recipient == null) {
                    continue;
                }

                MimeMessage handledMessage = null;

                try {
                    handledMessage = handleMessageForUser(mail,
                            userWorkflow.getUser(recipient.toString(), GetUserMode.CREATE_IF_NOT_EXIST));
                } catch (AddressException e) {
                    throw new MessagingException("Error strict handling message for user: " + recipient, e);
                } catch (HierarchicalPropertiesException e) {
                    throw new MessagingException("Error strict handling message for user: " + recipient, e);
                }

                if (handledMessage != null) {
                    messages.addMessage(handledMessage, recipient);
                } else {
                    /*
                     * The message was not encrypted, could not be decrypted etc.
                     */
                    messages.addAsIsRecipient(recipient);
                }
            }

            return messages;
        } catch (Exception e) {
            /*
             * We need to catch all to make sure messages will always be closed.
             * If not there is a change that temp files won't be deleted.
             */
            messages.close();

            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }

            if (e instanceof MessagingException) {
                throw (MessagingException) e;
            }

            throw new MessagingException("Error handling user", e);
        }
    }

    /*
     * Stores the message source and the recipients of the message.
     */
    private static class MimeSource {
        /*
         * Buffer that stores the raw mime source of the message. If the number of bytes 
         * written to ReadableOutputStreamBuffer exceeds the threshold, the bytes are 
         * written to a temporary file.
         */
        final ReadableOutputStreamBuffer mimeSource;

        /*
         * The recipients of the message
         */
        final Set<MailAddress> recipients = new HashSet<MailAddress>();

        MimeSource(ReadableOutputStreamBuffer mimeSource) {
            this.mimeSource = mimeSource;
        }

        Set<MailAddress> getRecipients() {
            return recipients;
        }

        ReadableOutputStreamBuffer getMimeSource() {
            return mimeSource;
        }
    }

    /*
     * Keeps track of all the resuling messages when messages are being handled by the
     * RecursiveSMIMEHandler.
     */
    private static class Messages {
        /*
         * The memory threshold of the buffer
         */
        final int threshold;

        /*
         * Mapping from SHA1 hash of the message to the message and recipients for the message
         */
        final Map<String, MimeSource> digestedMessages = new HashMap<String, MimeSource>();

        /*
         * Set of recipients to which the message should be send as-is, i.e., the message
         * was not an S/MIME message or the message could not be decrypted for the 
         * recipient
         */
        final Set<MailAddress> asIsRecipients = new HashSet<MailAddress>();

        /*
         * Keep track of all the buffers that must be closed
         */
        final Set<ReadableOutputStreamBuffer> buffers = new HashSet<ReadableOutputStreamBuffer>();

        Messages(int threshold) {
            this.threshold = threshold;
        }

        /*
         * Returns a message digest used to calculate the hash of the message
         */
        MessageDigest createDigest() throws IOException {
            try {
                return SecurityFactoryFactory.getSecurityFactory().createMessageDigest("SHA1");
            } catch (Exception e) {
                throw new IOException(e);
            }
        }

        /*
         * Returns the available threshold. 
         */
        int getThreshold() {
            long newThreshold = this.threshold;

            /*
             * calculate the available threshold based on the number of bytes 
             * currently used by the buffers
             */

            for (ReadableOutputStreamBuffer buffer : buffers) {
                /*
                 * If byte count of the buffer exceeds, 
                 */
                newThreshold = newThreshold - buffer.getByteCount();

                if (newThreshold < 0) {
                    newThreshold = 0;
                    break;
                }
            }

            return (int) newThreshold;
        }

        void releaseBuffer(ReadableOutputStreamBuffer buffer) {
            IOUtils.closeQuietly(buffer);
            buffers.remove(buffer);
        }

        /*
         * Closes all buffers. 
         * 
         * Note: close should always be called to make sure that all temporary files are cleaned up
         */
        void close() {
            for (ReadableOutputStreamBuffer buffer : buffers) {
                IOUtils.closeQuietly(buffer);
            }
        }

        MimeSource addMessage(MimeMessage message) throws MessagingException, IOException {
            message.saveChanges();

            ReadableOutputStreamBuffer mime = new ReadableOutputStreamBuffer(getThreshold());

            /*
             * Store a reference to make sure we can always close the buffers even 
             * when an exception has occurred.
             */
            buffers.add(mime);

            /*
             * Calculate a SHA1 of the message content so we can check wether this message
             * is the same message as another message. The reason for doing this is that 
             * when in strict mode, a message with multiple recipents is decrypted for 
             * each recipient. If the resulting message is the same, we do need to 
             * create multiple message but can add the recipient as a recipient of that
             * particular message. This is an optimization because with domain encryption
             * it can happen that a message has hundreds of recipients all encrypted with
             * the same key. We do not want such a message to be exploded into hundreds 
             * of separate messages.
             */
            DigestOutputStream digestOutputStream = new DigestOutputStream(new NullOutputStream(), createDigest());

            /*
             * We need to split the mime output because we only want to calculate the SHA1
             * of the message body. The main reason for this is that saveChanges will add
             * a new message-id for each message.
             */
            TeeOutputStream output = new TeeOutputStream(new BufferedOutputStream(mime),
                    new SkipHeadersOutputStream(digestOutputStream));

            message.writeTo(output);

            /*
             * We cannot close the mime stream yet because we need to be able to read the
             * content. We therefore need to flush all buffers.
             */
            digestOutputStream.flush();

            String messageDigest = HexUtils.hexEncode(digestOutputStream.getMessageDigest().digest());

            MimeSource mimeSource = digestedMessages.get(messageDigest);

            if (mimeSource == null) {
                mimeSource = new MimeSource(mime);

                digestedMessages.put(messageDigest, mimeSource);
            } else {
                /*
                 * We don't need to keep the mime content because the mime content
                 * has already been stored. We will release it to save memory.
                 */
                releaseBuffer(mime);
            }

            return mimeSource;
        }

        void addMessage(MimeMessage message, Collection<MailAddress> recipients) throws MessagingException {
            try {
                addMessage(message).getRecipients().addAll(recipients);
            } catch (IOException e) {
                throw new MessagingException("Error adding message.", e);
            }
        }

        void addMessage(MimeMessage message, MailAddress recipient) throws MessagingException {
            try {
                addMessage(message).getRecipients().add(recipient);
            } catch (IOException e) {
                throw new MessagingException("Error adding message.", e);
            }
        }

        void addAsIsRecipient(Collection<MailAddress> recipients) {
            asIsRecipients.addAll(recipients);
        }

        void addAsIsRecipient(MailAddress recipient) {
            asIsRecipients.add(recipient);
        }

        Collection<MimeSource> getMimeSources() {
            return digestedMessages.values();
        }

        public Set<MailAddress> getAsIsRecipients() {
            return asIsRecipients;
        }
    }

    /*
     * Extension of SMIMEInfoHandlerImpl which is used for adding security information to the subject 
     */
    private class ExtSMIMEInfoHandlerImpl extends SMIMEInfoHandlerImpl {
        /*
         * The tags that will be added to the subject (can be null)
         */
        private final SecurityInfoTags tags;

        /*
         * The MailID of the Mail
         */
        private final String mailID;

        public ExtSMIMEInfoHandlerImpl(Mail mail) {
            super(pKISecurityServices);

            DjigzoMailAttributes mailAttributes = DjigzoMailAttributesImpl.getInstance(mail);

            tags = mailAttributes.getSecurityInfoTags();
            mailID = mailAttributes.getMailID();
        }

        private void addTagToSubject(MimeMessage message, String tag) {
            try {
                String currentSubject = message.getSubject();

                String newSubject = String.format(subjectTemplate, StringUtils.defaultString(currentSubject),
                        StringUtils.defaultString(tag));

                message.setSubject(newSubject);
            } catch (Exception e) {
                logger.error("Error while appending text to subject");
            }
        }

        private boolean isSenderMismatch(MimeMessage message, Set<String> signers) {
            /*
             * Get the first From header and canonicalize it
             */
            String canonicalizedFrom = EmailAddressUtils.canonicalize(EmailAddressUtils
                    .getEmailAddress(EmailAddressUtils.getAddress(EmailAddressUtils.getFromQuietly(message))));

            if (signers != null) {
                for (String signer : signers) {
                    if (StringUtils.equals(canonicalizedFrom, EmailAddressUtils.canonicalize(signer))) {
                        /*
                         * On of the signers is equals to the from so no mismatch
                         */
                        return false;
                    }
                }
            }

            return true;
        }

        @Override
        protected void onSigned(MimeMessage message, SMIMEInspector sMIMEInspector, int level,
                boolean signatureValid, Set<String> signers) throws MessagingException {
            if (tags == null) {
                /*
                 * there is nothing to add
                 */
                return;
            }

            String tag;

            if (signatureValid) {
                /*
                 * If there is a mismatch between "sender" and email address from certificate, show the
                 * email address(es) of the signer(s). 
                 */
                if (isSenderMismatch(message, signers)) {
                    /*
                     * There is a mismatch between the from and the signers. We will therefore add the signers
                     * to the subject
                     */
                    try {
                        tag = String.format(tags.getSignedByValidTag(),
                                StringUtils.defaultString(StringUtils.join(signers, ", ")));
                    } catch (Exception e) {
                        logger.error("Invalid format string", e);

                        tag = "<invalid format string>";
                    }
                } else {
                    tag = tags.getSignedValidTag();
                }
            } else {
                tag = tags.getSignedInvalidTag();
            }

            addTagToSubject(message, tag);
        }

        @Override
        protected void onEncrypted(MimeMessage message, SMIMEInspector sMIMEInspector, int level, boolean decrypted)
                throws MessagingException {
            /*
             * Log the decryption event (see https://jira.djigzo.com/browse/GATEWAY-42).
             * 
             * Note: we will also log the MailID otherwise it's unclear for which message the log entry is
             */
            if (decrypted) {
                logger.info("Message with MailID {} has been decrypted.", StringUtils.defaultString(mailID));
            }

            if (tags == null) {
                /*
                 * there is nothing to add
                 */
                return;
            }

            if (decrypted) {
                addTagToSubject(message, tags.getDecryptedTag());
            }
        }
    }
}