Java tutorial
/* * 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); } }