Java tutorial
/* * Copyright (c) 2009-2011, 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.cert.X509Certificate; import java.util.Set; import javax.mail.Address; import javax.mail.MessagingException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import javax.mail.internet.ParseException; import mitm.application.djigzo.NamedCertificateCategories; import mitm.application.djigzo.NamedCertificatesUserPreferencesManager; import mitm.application.djigzo.User; import mitm.application.djigzo.UserPreferences; import mitm.application.djigzo.UserProperties; import mitm.application.djigzo.james.DjigzoMailAttributes; import mitm.application.djigzo.james.DjigzoMailAttributesImpl; import mitm.application.djigzo.james.MailAddressUtils; import mitm.application.djigzo.james.MailetUtils; import mitm.application.djigzo.james.MessageOriginatorIdentifier; import mitm.application.djigzo.relay.RelayBounceMode; import mitm.application.djigzo.relay.RelayException; import mitm.application.djigzo.relay.RelayHandler; import mitm.application.djigzo.relay.RelayInfo; import mitm.application.djigzo.relay.RelayStep; import mitm.application.djigzo.relay.RelayUserInfo; import mitm.application.djigzo.relay.RelayUserInfoImpl; import mitm.application.djigzo.relay.RelayUserInfoProvider; import mitm.application.djigzo.relay.RelayValidator; import mitm.application.djigzo.service.SystemServices; import mitm.application.djigzo.workflow.UserWorkflow; import mitm.common.cache.KeyCache; import mitm.common.hibernate.DatabaseActionExecutor; import mitm.common.hibernate.DatabaseActionExecutorBuilder; import mitm.common.hibernate.DatabaseException; import mitm.common.hibernate.DatabaseVoidAction; import mitm.common.hibernate.SessionManager; import mitm.common.mail.BodyPartUtils; import mitm.common.mail.EmailAddressUtils; import mitm.common.properties.HierarchicalPropertiesException; import mitm.common.security.PKISecurityServices; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.lang.mutable.MutableObject; 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.hibernate.exception.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Relay mailet is used to relay external messages. This is for example used with Djigzo for BlackBerry. * * sender : The sender of the relayed message. * preventReplay : Check whether a relay message has already been relayed. * relayProcessor : The next processor for the relayed message * bounceProcessor : The next processor for the (original) message when bouncing. * passThrough : If false and a message is relayed the original will be discarded * catchRuntimeExceptions : If true all RunTimeExceptions are caught (default: true) * catchErrors : If true all Errors are caught (default: true) * * @author Martijn Brinkers * */ public class Relay extends AbstractDjigzoMailet { private final static Logger logger = LoggerFactory.getLogger(Relay.class); /* * The mailet initialization parameters used by this mailet. */ private enum Parameter { SENDER("sender"), PASS_THROUGH("passThrough"), RELAY_PROCESSOR("relayProcessor"), BOUNCE_PROCESSOR( "bounceProcessor"), PREVENT_REPLAY("preventReplay"); private String name; private Parameter(String name) { this.name = name; } @Override public String toString() { return name; } }; /* * The special addresses of the sender of the relay'd message */ private enum SpecialAddress { NULL("null"), SAME_AS_RELAY("sameAsRelay"), SAME_AS_FROM("sameAsFrom"); private String name; private SpecialAddress(String name) { this.name = name; } public static SpecialAddress fromName(String name) { for (SpecialAddress sender : SpecialAddress.values()) { if (sender.name.equals(name)) { return sender; } } return null; } @Override public String toString() { return name; } }; /* * RelayValidator implementation that checks whether the relay message is already relayed by checking * whether the nonce is in the cache (the nonce should be unique). */ protected static class RelayValidatorImpl implements RelayValidator { /* * Key cache is used to prevent 'replay' attacks. */ private final KeyCache keyCache; public RelayValidatorImpl(KeyCache keyCache) { this.keyCache = keyCache; } /* * Checks if the relay message has already been relayed by checking of the combination of timestamp and * nonce is already placed in the cache. */ @Override public void validate(RelayInfo relayInfo) throws RelayException { StrBuilder keyBuilder = new StrBuilder(256); /* * The key is a combination of nonce, timestamp and from */ keyBuilder.append(relayInfo.getNonce()); keyBuilder.append(relayInfo.getTimestamp()); keyBuilder.append(relayInfo.getFrom()); String key = keyBuilder.toString(); if (StringUtils.isEmpty(key)) { throw new RelayException(RelayStep.UNKNOWN, "key is empty."); } if (!keyCache.add(key)) { throw new RelayException(RelayStep.DUPLICATE_MESSAGE, "Duplicate relay message detected. key: " + key); } } } /* * If true the source message will continue to be used. If false the source message will * be ghost'ed. */ private boolean passThrough = false; /* * The processor to for a relayed message */ private String relayProcessor; /* * The processor to for a bounced message */ private String bounceProcessor; /* * The sender of the relay'd message */ private String sender = SpecialAddress.SAME_AS_FROM.toString(); /* * If true the Key Cache is used to prevent 'replay attacks' */ private boolean preventReplay = true; /* * All PKI related services */ private PKISecurityServices pKISecurityServices; /* * manages database sessions */ private SessionManager sessionManager; /* * Used to execute database actions in a transaction */ private DatabaseActionExecutor actionExecutor; /* * Is used to identify the 'sender' (from or enveloped sender or...) of the message */ private MessageOriginatorIdentifier messageOriginatorIdentifier; /* * Used for adding and retrieving users */ private UserWorkflow userWorkflow; /* * RelayValidator is used to detect relay 'replay attacks' */ private RelayValidator relayValidator; /* * Helper class to set/get named certificate to/from UserPreferences */ private NamedCertificatesUserPreferencesManager namedCertificatesManager; @Override protected Logger getLogger() { return logger; } private void initRelayProcessor() throws MessagingException { relayProcessor = getInitParameter(Parameter.RELAY_PROCESSOR.name); if (StringUtils.isBlank(relayProcessor)) { throw new MessagingException("relayProcessor must be specified."); } } private void initBounceProcessor() throws MessagingException { bounceProcessor = getInitParameter(Parameter.BOUNCE_PROCESSOR.name); if (StringUtils.isBlank(bounceProcessor)) { throw new MessagingException("bounceProcessor must be specified."); } } private void initPassThrough() { String param = getInitParameter(Parameter.PASS_THROUGH.name); if (param != null) { passThrough = BooleanUtils.toBoolean(param); } } private void initPreventReplay() { String param = getInitParameter(Parameter.PREVENT_REPLAY.name); if (param != null) { preventReplay = BooleanUtils.toBoolean(param); } } private void initSender() throws ParseException { String param = getInitParameter(Parameter.SENDER.name); if (param != null) { /* * Check if sender is a special address or just an email address */ SpecialAddress specialAddress = SpecialAddress.fromName(param); if (specialAddress != null) { sender = param; } else { /* * Not a special address so it should be a valid email address. * Validate it to make sure it's a valid email address. */ sender = new MailAddress(param).toString(); } } } @Override final public void initMailet() throws MessagingException { getLogger().info("Initializing mailet: " + getMailetName()); initSender(); initRelayProcessor(); initBounceProcessor(); initPassThrough(); initPreventReplay(); StrBuilder sb = new StrBuilder(); sb.append("sender: "); sb.append(sender); sb.append("; "); sb.append("relayProcessor: "); sb.append(relayProcessor); sb.append("; "); sb.append("bounceProcessor: "); sb.append(bounceProcessor); sb.append("; "); sb.append("passThrough: "); sb.append(passThrough); sb.append("; "); sb.append("preventReplay: "); sb.append(preventReplay); getLogger().info(sb.toString()); sessionManager = SystemServices.getSessionManager(); actionExecutor = DatabaseActionExecutorBuilder.createDatabaseActionExecutor(sessionManager); pKISecurityServices = SystemServices.getPKISecurityServices(); messageOriginatorIdentifier = SystemServices.getMessageOriginatorIdentifier(); userWorkflow = SystemServices.getUserWorkflow(); relayValidator = new RelayValidatorImpl(SystemServices.getKeyCache()); namedCertificatesManager = new NamedCertificatesUserPreferencesManager(NamedCertificateCategories.RELAY); } @Override public void serviceMail(final Mail mail) { try { MimeMessage sourceMessage = mail.getMessage(); /* * The relay message should be an attached message */ final MimeMessage relayMessage = sourceMessage = BodyPartUtils.searchForRFC822(sourceMessage); if (relayMessage != null) { /* * Do not retry on a ConstraintViolationException because if retried it can * happen that multiple relay messages will be sent. * */ actionExecutor.executeTransaction(new DatabaseVoidAction() { @Override public void doAction(Session session) throws DatabaseException { Session previousSession = sessionManager.getSession(); sessionManager.setSession(session); try { handleMessageAction(session, mail, relayMessage); } finally { sessionManager.setSession(previousSession); } } }); } else { logger.debug("Relay message not found. Sending as-is."); } } catch (ConstraintViolationException e) { /* * Can happen when multiple messages are sent to a new user in a burst. The * user is added to the database in multiple threads. */ getLogger().warn("ConstraintViolationException. " + ExceptionUtils.getRootCauseMessage(e)); } 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 RelayUserInfo createRelayUserInfo(User user) throws HierarchicalPropertiesException { RelayUserInfo userInfo = null; if (user != null) { UserPreferences userPreferences = user.getUserPreferences(); /* * Get the relay certificates for the user */ Set<X509Certificate> relayCertificates = namedCertificatesManager.getNamedCertificates(userPreferences); UserProperties properties = userPreferences.getProperties(); userInfo = new RelayUserInfoImpl(user.getEmail(), relayCertificates, properties.isRelayAllowed(), properties.getRelayValidityInterval(), properties.getRelayBounceMode()); } return userInfo; } private MailAddress getFrom(MimeMessage message) throws MessagingException { Address[] froms = message.getFrom(); if (froms == null || froms.length == 0) { return null; } return new MailAddress(EmailAddressUtils.getEmailAddress(froms[0])); } private void setSender(MailImpl newMail, MimeMessage messageToRelay) throws MessagingException { /* * Check if sender is a special address or just an email address */ SpecialAddress specialAddress = SpecialAddress.fromName(sender); if (specialAddress != null) { switch (specialAddress) { case NULL: newMail.setSender(null); break; case SAME_AS_FROM: newMail.setSender(getFrom(messageToRelay)); break; default: logger.warn("Unhandled special address: " + specialAddress); } } else { /* * Not a special address so it should be a valid email address. */ newMail.setSender(new MailAddress(sender)); } } private void sendRelayMessage(Mail sourceMail, MimeMessage messageToRelay, Set<String> recipients) throws MessagingException { logger.debug("Sending relay message"); /* * 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(MailAddressUtils.toMailAddressList(recipients)); newMail.setMessage(messageToRelay); newMail.setState(relayProcessor); setSender(newMail, messageToRelay); getMailetContext().sendMail(newMail); } finally { newMail.dispose(); } } private void handleBlackBerryRelayException(RelayException e, RelayUserInfo userInfo, Mail sourceMail) throws DatabaseException { String errorMessage = "Error relaying for user " + (userInfo != null ? userInfo.getEmail() : "<unknown>") + ". " + e.getMessage(); logger.debug(errorMessage, e); /* * Log most exceptions on warning level (invalid can be thrown with any email message so we do not want * to clutter the logs). */ if (RelayStep.INVALID != e.getRelayStep()) { logger.warn(errorMessage); } boolean bounce = false; if (userInfo != null) { if (userInfo.getBounceMode() == RelayBounceMode.ON_RELAYING_DENIED) { switch (e.getRelayStep()) { case RELAYING_NOT_ALLOWED: case MESSAGE_EXPIRED: case INVALID_RECIPIENT: bounce = true; default: break; } } else if (userInfo.getBounceMode() == RelayBounceMode.ON_INVALID_RECIPIENT) { bounce = (e.getRelayStep() == RelayStep.INVALID_RECIPIENT); } } if (bounce) { /* * Set the error message as a Mail property so we can use it in the bounce * message template */ DjigzoMailAttributes attributes = new DjigzoMailAttributesImpl(sourceMail); attributes.setMessage(e.getMessage()); /* * Send the message to the bounce processor */ sourceMail.setState(bounceProcessor); } } private void handleMessageAction(Session session, Mail sourceMail, MimeMessage relayMessage) throws DatabaseException { final MutableObject userInfoContainer = new MutableObject(); RelayUserInfoProvider infoProvider = new RelayUserInfoProvider() { @Override public RelayUserInfo getRelayUserInfo(MimeMessage message) throws MessagingException, RelayException { RelayUserInfo userInfo = null; InternetAddress originator = messageOriginatorIdentifier.getOriginator(message); if (originator != null) { String userEmail = originator.getAddress(); logger.debug("Getting relay info for " + userEmail); userEmail = EmailAddressUtils.canonicalizeAndValidate(userEmail, false); try { User user = userWorkflow.getUser(userEmail, UserWorkflow.GetUserMode.CREATE_IF_NOT_EXIST); userInfo = createRelayUserInfo(user); /* * Save the userInfo because we need it later on */ userInfoContainer.setValue(userInfo); } catch (HierarchicalPropertiesException e) { throw new RelayException(e); } } return userInfo; } }; RelayHandler handler = new RelayHandler(pKISecurityServices, infoProvider); if (preventReplay) { handler.setAdditionalRelayValidator(relayValidator); } try { MimeMessage messageToRelay = handler.handleRelayMessage(relayMessage); if (messageToRelay != null) { sendRelayMessage(sourceMail, messageToRelay, handler.getRecipients()); /* * ghost the source mail if not passThrough */ if (!passThrough) { sourceMail.setState(Mail.GHOST); } } } catch (RelayException e) { handleBlackBerryRelayException(e, (RelayUserInfo) userInfoContainer.getValue(), sourceMail); } catch (MessagingException e) { throw new DatabaseException(e); } catch (IOException e) { throw new DatabaseException(e); } } }