mitm.djigzo.web.pages.portal.pdf.PDFReply.java Source code

Java tutorial

Introduction

Here is the source code for mitm.djigzo.web.pages.portal.pdf.PDFReply.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 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 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.djigzo.web.pages.portal.pdf;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import mitm.application.djigzo.DjigzoException;
import mitm.application.djigzo.james.mailets.PDFReplyURLBuilder;
import mitm.common.cache.CacheEntry;
import mitm.common.cache.CacheException;
import mitm.common.cache.ContentCache;
import mitm.common.cache.FileStreamCacheEntry;
import mitm.common.cache.RateCounter;
import mitm.common.locale.CharacterEncoding;
import mitm.common.mail.EmailAddressUtils;
import mitm.common.properties.HierarchicalPropertiesException;
import mitm.common.util.Check;
import mitm.common.util.MiscStringUtils;
import mitm.common.util.URLBuilderException;
import mitm.common.ws.WebServiceCheckedException;
import mitm.djigzo.web.asos.PDFReplyState;
import mitm.djigzo.web.entities.User;
import mitm.djigzo.web.entities.UserManager;
import mitm.djigzo.web.services.DisableHttpCache;
import mitm.djigzo.web.utils.HttpServletUtils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailAttachment;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.MultiPartEmail;
import org.apache.commons.mail.SimpleEmail;
import org.apache.tapestry5.Asset;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.ApplicationState;
import org.apache.tapestry5.annotations.BeginRender;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.IncludeJavaScriptLibrary;
import org.apache.tapestry5.annotations.IncludeStylesheet;
import org.apache.tapestry5.annotations.Path;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.TextArea;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Value;
import org.apache.tapestry5.services.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The PDF reply page on which a user can type a reply and add attachments.
 *
 * Note: localeSelector.js is explicitly loaded to make sure we can override "afterSelectLocale" javascript function.
 * We need to override "afterSelectLocale" to force a "save draft" before the locale is changed otherwise the user
 * looses anything added to the body when the locale is changed.
 *
 * @author Martijn Brinkers
 *
 */
@IncludeStylesheet({ "context:styles/pages/portal/pdf/pdfreply.css" })
@IncludeJavaScriptLibrary({ "../../../components/localeSelector.js", "pdfReply.js" })
public class PDFReply {
    private final static Logger logger = LoggerFactory.getLogger(PDFReply.class);

    /*
     * The character encoding used to encode the body with (for storing into the cache)
     */
    private static final String BODY_ENCODING = "UTF-8";

    @SuppressWarnings("unused")
    @Inject
    @Path("context:images/logo.png")
    @Property
    private Asset logoAsset;

    /*
     * Maximum number of times in a time frame a user can reply to a specific message (DOS protection)
     */
    @Inject
    @Value("${web.max-reply-rate}")
    private int maxReplyRate;

    /*
     * the charset to use for the PDF reply
     */
    @Inject
    @Value("${web.pdf.reply.charset}")
    private String replyBodyCharset;

    /*
     * The time a reply to a message stays in the rateCounter (for DOS protection)
     */
    @Inject
    @Value("${web.reply-rate-lifetime}")
    private int replyRateLifetime;

    /*
     * The submit buttons on the form
     */
    private enum SubmitButton {
        SEND, CANCEL, SAVE_DRAFT
    };

    @Component(id = "form", parameters = { "clientValidation=false" })
    private Form form;

    @Component(id = "body", parameters = { "value=body" })
    private TextArea bodyField;

    @Inject
    private UserManager userManager;

    @Inject
    private ComponentResources resources;

    @Inject
    private Request request;

    /*
     * We use the HttpServletRequest to get the session-id.
     */
    @Inject
    private HttpServletRequest httpRequest;

    @Inject
    private Messages messages;

    /*
     * Cache which is used for storing uploaded files.
     */
    @Inject
    private ContentCache cache;

    /*
     * RateCounter is used to track concurrent 'logins' (used for DOS protection).
     */
    @Inject
    private RateCounter rateCounter;

    /*
     * True if there was an error sending the message.
     */
    @Persist(PersistenceConstants.FLASH)
    private boolean errorSending;

    /*
     * The message if errorSending is true.
     */
    @Persist(PersistenceConstants.FLASH)
    private String errorMessage;

    /*
     * Stores all the relevant reply parameters (from the reply request created by the user when reply was
     * clicked in the PDF).
     */
    @ApplicationState
    private PDFReplyState replyState;

    /*
     * True if the user is already editing a reply message. User will be redirected to the message if the
     * user is already editing a message.
     */
    @Persist
    private boolean editingMessage;

    /*
     * Will store which submit button was pressed.
     */
    private SubmitButton submitButton;

    private boolean isEditingMessage() {
        return editingMessage;
    }

    public Class<?> onActivate() throws IOException {
        Class<?> returnPage = handleReplyLink();

        if (returnPage == null && !replyState.isValidRequest()) {
            returnPage = handleInvalidRequest();
        }

        return returnPage;
    }

    @BeginRender
    @DisableHttpCache
    public void beginRender() {
        // empty on purpose. We need beginRender to set @DisableHttpCache
    }

    /*
     * If the user replies to the PDF a link will be openend from the PDF containing the reply parameters in the form:
     *       reply?userEmail=b&recipientEmail=b&... etc.
     *
     * If the URL contains these parameters we will handle them.
     *
     * Returns True if handled, False if not
     */
    private Class<?> handleReplyLink() throws IOException {
        /*
         * Get the parameters from the HTTP request
         */
        String envelope = request.getParameter(PDFReplyURLBuilder.REPLY_ENVELOPE_PARAMETER);
        String hmac = request.getParameter(PDFReplyURLBuilder.HMAC_PARAMETER);

        if (envelope == null || hmac == null) {
            /*
             * It's not an initial reply from the user clicking the PDF
             */
            return null;
        }

        /*
         * Check if the user is already editing a message
         */
        if (isEditingMessage()) {
            /*
             * Redirect to PDFReplyActive to notify the user that the user is already editing a reply
             */
            return PDFReplyActive.class;
        }

        /*
         * Store the activation context in the application state object.
         */
        replyState.loadFromEnvelope(envelope, hmac);

        /*
         * Only accept valid requests (keyed MAC should be correct).
         */
        if (!replyState.isValidRequest()) {
            return handleInvalidRequest();
        }

        /*
         * As a protection against a user DOS'ing the server we will keep track of the number of replies to a
         * specific message. If there are too many replies in a specific time frame the reply will not be
         * accepted.
         */
        if (isMaxReplyRateReached()) {
            logger.warn("Max. concurrent session is reached for message with key: " + replyState.getMessageKey());

            return handleTooManyRequests();
        }

        /*
         * The user is now editing a message
         */
        editingMessage = true;

        /*
         * Redirect back to this page but this time without the context parameters. The context parameters are
         * stored in session and will be used.
         */
        return PDFReply.class;
    }

    private User getUser(String email) throws WebServiceCheckedException {
        String filteredEmail = EmailAddressUtils.canonicalizeAndValidate(email, true);

        if (filteredEmail == null) {
            return null;
        }

        return userManager.getUser(filteredEmail, true /* dummy if not exist */);
    }

    /*
     * Returns a unique identifier for a message which will be used to identify the attachment in the cache.
     */
    private String getCacheKey() {
        return httpRequest.getSession().getId() + replyState.getMessageKey();
    }

    /*
     * Returns true if the maximum concurrent sessions for one messageKey is reached.
     */
    private boolean isMaxReplyRateReached() {
        boolean maxReached = rateCounter.getItemCount(replyState.getMessageKey()) >= maxReplyRate;

        if (!maxReached) {
            rateCounter.addKey(replyState.getMessageKey(), httpRequest.getSession().getId(), replyRateLifetime);
        }

        return maxReached;
    }

    private Class<?> invalidateSessionAndRedirect(Class<?> redirectPage) throws IOException {
        HttpServletUtils.invalidateSession(httpRequest);

        return redirectPage;
    }

    /*
     * Returns a cached body or attachment identified by the id.
     */
    private CacheEntry getUniqueCacheEntry(String id) throws CacheException {
        CacheEntry entry = cache.getEntry(getCacheKey(), id);

        return entry;
    }

    public void setBody(String body) throws IOException, CacheException {
        if (body == null) {
            body = "";
        }

        InputStream input = IOUtils.toInputStream(body, BODY_ENCODING);

        addAttachment(PDFAttachments.BODY_ATTACHMENT_ID, PDFAttachments.BODY_ATTACHMENT_ID, input);
    }

    public String getBody() throws CacheException, UnsupportedEncodingException {
        String body = "";

        CacheEntry bodyAttachment = getUniqueCacheEntry(PDFAttachments.BODY_ATTACHMENT_ID);

        if (bodyAttachment != null) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            bodyAttachment.writeTo(bos);
            body = new String(bos.toByteArray(), BODY_ENCODING);
        }

        return body;
    }

    protected void onSelectedFromSend() {
        submitButton = SubmitButton.SEND;
    }

    protected void onSelectedFromCancel() {
        submitButton = SubmitButton.CANCEL;
    }

    protected void onSelectedFromSaveDraft() {
        submitButton = SubmitButton.SAVE_DRAFT;
    }

    private void addAttachment(String id, String filename, InputStream input) throws IOException, CacheException {
        CacheEntry entry = getUniqueCacheEntry(id);

        if (entry == null) {
            entry = new FileStreamCacheEntry(id);

            entry.setObject(filename);
            cache.addEntry(getCacheKey(), entry);
        }

        entry.store(input);
    }

    /*
     * Add all the attachments from the cache to the email.
     */
    private void addAttachments(Email email, List<CacheEntry> entries, List<File> filesToDelete)
            throws IOException, FileNotFoundException, CacheException, EmailException {
        for (CacheEntry entry : entries) {
            /*
             * The body entry is not an attachment so we should skip it
             */
            if (PDFAttachments.BODY_ATTACHMENT_ID.equals(entry.getId())) {
                continue;
            }

            /*
             * Store the attachment in a temporary file
             */
            File file = File.createTempFile("attachment", null);

            /*
             * register the temporary file so we can delete it after the message has been sent
             */
            filesToDelete.add(file);

            FileOutputStream fos = new FileOutputStream(file);

            try {
                entry.writeTo(fos);
            } finally {
                IOUtils.closeQuietly(fos);
            }

            EmailAttachment attachment = new EmailAttachment();

            attachment.setPath(file.getPath());
            attachment.setDisposition(EmailAttachment.ATTACHMENT);
            attachment.setName(entry.getObject().toString());
            attachment.setDescription(null);

            ((MultiPartEmail) email).attach(attachment);
        }
    }

    private String getCharsetForBody(String body) {
        /*
         * Return replyBodyCharset if the body contains non-ascii characters
         */
        return MiscStringUtils.isPrintableAscii(body) ? CharacterEncoding.US_ASCII : replyBodyCharset;
    }

    private void sendMessage() throws EmailException, WebServiceCheckedException, CacheException, IOException,
            HierarchicalPropertiesException {
        throw new WebServiceCheckedException("PDF Encryption is not supported.");

        /*List<CacheEntry> entries = cache.getAllEntries(getCacheKey());
            
            
        // If we have attachments the message should be multipart
            
        Email email = entries.size() > 1 ? email = new MultiPartEmail() : new SimpleEmail();
            
            
        // Default to SMTP server on localhost if not set
            
        if (StringUtils.isEmpty(System.getProperties().getProperty("mail.smtp.host"))) {
           email.setHostName("127.0.0.1");
        }
            
        User user = getUser(replyState.getUserEmail());
            
        Check.notNull(user, "user");
            
        String sender = user.getPreferences().getProperties().getPdfReplySender();
            
        if (sender == null) {
           throw new EmailException("user " + replyState.getUserEmail() + " has no PDF sender.");
        }
            
            
        // We will set the from to a different user to make sure we can set the properties of the from.
        // The reply will be set to the recipient of the encrypted PDF.
            
        email.addTo(replyState.getRecipientEmail());
        email.setFrom(sender, messages.format("from", replyState.getFromEmail()));
        email.addReplyTo(replyState.getFromEmail());
        email.setSubject(getSubject());
            
        String body = getBody();
            
        // Make sure the body is correctly encoded (default UTF-8 for non-ascii)
            
        email.setCharset(getCharsetForBody(body));
        email.setMsg(body);
            
        email.setBounceAddress(sender);
            
            
        // We need to temporarily store some files for the attachment and delete them after we
        // have sent the message
            
        List<File> filesToDelete = new LinkedList<File>();
            
        try {
           addAttachments(email, entries, filesToDelete);
            
           email.send();
            
           // Remove body and attachments
            
           cache.removeAllEntries(getCacheKey());
        }
        finally
        {
            
           // Delete all temporary files created for attachment
            
           for (File file : filesToDelete)
           {
        if (!file.delete()) {
           logger.error("Unable to delete temporary file: " + file);
        }
           }
        }*/
    }

    private Class<?> handleInvalidRequest() throws IOException {
        return invalidateSessionAndRedirect(PDFInvalidRequest.class);
    }

    private Class<?> handleTooManyRequests() throws IOException {
        return invalidateSessionAndRedirect(PDFTooManyRequests.class);
    }

    /*
     * Called when the send button is clicked
     */
    private Class<?> handleSend()
            throws CacheException, IOException, WebServiceCheckedException, HierarchicalPropertiesException {
        String body = getBody();

        if (StringUtils.isEmpty(body)) {
            form.recordError(bodyField, messages.get("empty-message-body"));
        } else {
            try {
                sendMessage();

                return invalidateSessionAndRedirect(PDFReplySent.class);
            } catch (EmailException e) {
                logger.error("Error sending message.", e);

                errorSending = true;
                errorMessage = e.getMessage();
            }
        }

        return null;
    }

    /*
     * Called when the cancel button is clicked
     */
    public Class<?> handleCancel() throws CacheException, IOException {
        /*
         * If cancel button is pressed we should remove the body and attachments
         */
        cache.removeAllEntries(getCacheKey());

        return invalidateSessionAndRedirect(PDFReplyCanceled.class);
    }

    public Class<?> onValidateForm() throws WebServiceCheckedException, HierarchicalPropertiesException,
            URLBuilderException, DjigzoException {
        try {
            if (!replyState.isValidRequest()) {
                return handleInvalidRequest();
            }

            if (submitButton == null) {
                return null;
            }

            switch (submitButton) {
            case SEND:
                return handleSend();
            case CANCEL:
                return handleCancel();
            case SAVE_DRAFT:
                /* do nothing */ break;

            default:
                throw new IllegalArgumentException("Unhandled submitButton " + submitButton);
            }

            return null;
        } catch (UnsupportedEncodingException e) {
            throw new DjigzoException(e);
        } catch (CacheException e) {
            throw new DjigzoException(e);
        } catch (IOException e) {
            throw new DjigzoException(e);
        }
    }

    /*
     * getter called from .tml file
     */
    public String getFrom() {
        return replyState.getFromEmail();
    }

    /*
     * getter called from .tml file
     */
    public String getTo() {
        return replyState.getRecipientEmail();
    }

    /*
     * getter called from .tml file
     */
    public String getSubject() {
        String result = replyState.getSubject();

        String replyPrefix = messages.get("subject-reply-prefix");

        /*
         * Add a Re: prefix to the subject if it does not already contain the prefix.
         */
        if (!StringUtils.startsWithIgnoreCase(result, replyPrefix)) {
            result = replyPrefix + " " + result;
        }

        return result;
    }

    /*
     * getter called from .tml file
     */
    public boolean isErrorSending() {
        return errorSending;
    }

    /*
     * getter called from .tml file
     */
    public String getErrorMessage() {
        return errorMessage;
    }

    /*
     * getter called from .tml file used for the attachment iframe.
     */
    public Link getAttachmentsLink() {
        return resources.createPageLink(PDFAttachments.class, false);
    }
}