/* * Copyright (c) 2011 Lockheed Martin Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.eurekastreams.server.service.email; import java.io.IOException; import java.util.List; import java.util.Map; import javax.mail.Address; import javax.mail.Message; import javax.mail.Message.RecipientType; import javax.mail.MessagingException; import javax.mail.internet.InternetAddress; import org.eurekastreams.commons.actions.context.DefaultPrincipal; import org.eurekastreams.commons.actions.context.PrincipalActionContext; import org.eurekastreams.commons.actions.context.service.ServiceActionContext; import org.eurekastreams.commons.actions.service.ServiceAction; import org.eurekastreams.commons.actions.service.TaskHandlerServiceAction; import org.eurekastreams.commons.exceptions.ExecutionException; import org.eurekastreams.commons.exceptions.ValidationException; import org.eurekastreams.commons.server.UserActionRequest; import org.eurekastreams.commons.server.service.ActionController; import org.eurekastreams.server.persistence.mappers.DomainMapper; import org.eurekastreams.server.search.modelview.PersonModelView; import org.springframework.beans.factory.BeanFactory; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; /** * Responsible for all processing of a single received email message. */ public class MessageProcessor { /** For getting the user's content from the message. */ private final MessageContentExtractor messageContentExtractor; /** Determines which action to execute. */ private final ActionSelectorFactory actionSelector; /** Instance of {@link ActionController} used to run actions. */ private final ActionController serviceActionController; /** The context from which this service can load action beans. */ private final BeanFactory beanFactory; /** For decoding the token. */ private final TokenEncoder tokenEncoder; /** Parses the token content. */ private final TokenContentFormatter tokenContentFormatter; /** Transaction manager (to allow calling mappers). */ private final PlatformTransactionManager transactionMgr; /** DAO to get a user's person ID given their email address. */ private final DomainMapper<String, Long> personIdByEmailDao; /** DAO to get a user's crypto key given their person ID. */ private final DomainMapper<Long, byte[]> userKeyByIdDao; /** DAO to get a person by ID. */ private final DomainMapper<Long, PersonModelView> personDao; /** Responds to messages that failed execution with result status. */ private final MessageReplier messageReplier; /** Address messages must be sent to. */ private final String requiredToAddress; /** Text address must begin with to be the desired To address. */ private final String toEmailRequiredStart; /** Text address must end with to be the desired To address. */ private final String toEmailRequiredEnd; /** * Constructor. * * @param inMessageContentExtractor * For getting the user's content from the message. * @param inActionSelector * Determines which action to execute. * @param inServiceActionController * Instance of {@link ActionController} used to run actions. * @param inBeanFactory * The context from which this service can load action beans. * @param inTokenEncoder * For decoding the token. * @param inTokenContentFormatter * Parses the token content. * @param inTransactionMgr * Transaction manager (to allow calling mappers). * @param inPersonIdByEmailDao * DAO to get a user's person ID given their email address. * @param inUserKeyByIdDao * DAO to get a user's crypto key given their person ID. * @param inPersonDao * DAO to get a person by ID. * @param inMessageReplier * Responds to messages that failed execution with result status. * @param inRequiredToAddress * Address messages must be sent to. */ public MessageProcessor(final MessageContentExtractor inMessageContentExtractor, final ActionSelectorFactory inActionSelector, final ActionController inServiceActionController, final BeanFactory inBeanFactory, final TokenEncoder inTokenEncoder, final TokenContentFormatter inTokenContentFormatter, final PlatformTransactionManager inTransactionMgr, final DomainMapper<String, Long> inPersonIdByEmailDao, final DomainMapper<Long, byte[]> inUserKeyByIdDao, final DomainMapper<Long, PersonModelView> inPersonDao, final MessageReplier inMessageReplier, final String inRequiredToAddress) { messageContentExtractor = inMessageContentExtractor; actionSelector = inActionSelector; serviceActionController = inServiceActionController; beanFactory = inBeanFactory; tokenEncoder = inTokenEncoder; tokenContentFormatter = inTokenContentFormatter; transactionMgr = inTransactionMgr; personIdByEmailDao = inPersonIdByEmailDao; userKeyByIdDao = inUserKeyByIdDao; personDao = inPersonDao; messageReplier = inMessageReplier; requiredToAddress = inRequiredToAddress; int pos = inRequiredToAddress.indexOf('@'); toEmailRequiredStart = inRequiredToAddress.substring(0, pos) + "+"; toEmailRequiredEnd = inRequiredToAddress.substring(pos); } /** * Processes the message. * * @param message * Message to process. * @param inResponseMessages * List to add response messages to. * @return If message was processed. * @throws MessagingException * On error. * @throws IOException * On error. */ public boolean execute(final Message message, final List<Message> inResponseMessages) throws MessagingException, IOException { String token = getToken(message); if (token == null) { return false; } String fromAddress = getFromAddress(message); // get the sender and sender's key DefaultTransactionDefinition transDef = new DefaultTransactionDefinition(); transDef.setName("TokenAddressMessageAuthenticator"); transDef.setReadOnly(false); TransactionStatus transStatus = transactionMgr.getTransaction(transDef); byte[] key; Long personId; PersonModelView person; try { personId = personIdByEmailDao.execute(fromAddress); key = userKeyByIdDao.execute(personId); person = personDao.execute(personId); } finally { transactionMgr.commit(transStatus); } // decrypt and unpack the token Map<String, Long> tokenData = getTokenData(token, key); // get the message content String content = messageContentExtractor.extract(message); // choose action to execute UserActionRequest actionSelection = actionSelector.select(tokenData, content, person); // execute action executeAction(message, actionSelection, person, inResponseMessages); return true; } /** * Decrypts and unpacks the token. * * @param token * Raw token. * @param key * User's key. * @return Data contained in token. */ Map<String, Long> getTokenData(final String token, final byte[] key) { String tokenConent = tokenEncoder.decode(token, key); if (tokenConent == null) { throw new ValidationException("Cannot decrypt token for user."); } Map<String, Long> tokenData = tokenContentFormatter.parse(tokenConent); if (tokenData == null) { throw new ValidationException("Cannot parse token."); } return tokenData; } /** * Execute the selected action. * * @param message * The original message. * @param actionSelection * The selected action. * @param person * The user. * @param inResponseMessages * List to add response messages to. */ void executeAction(final Message message, final UserActionRequest actionSelection, final PersonModelView person, final List<Message> inResponseMessages) { try { Object springBean = beanFactory.getBean(actionSelection.getActionKey()); PrincipalActionContext actionContext = new ServiceActionContext(actionSelection.getParams(), new DefaultPrincipal(person.getAccountId(), person.getOpenSocialId(), person.getId())); actionContext.setActionId(actionSelection.getActionKey()); if (springBean instanceof ServiceAction) { ServiceAction action = (ServiceAction) springBean; serviceActionController.execute(actionContext, action); } else if (springBean instanceof TaskHandlerServiceAction) { TaskHandlerServiceAction action = (TaskHandlerServiceAction) springBean; serviceActionController.execute(actionContext, action); } else { throw new ExecutionException("Bean '" + actionSelection.getActionKey() + "' is not an executable action"); } } catch (RuntimeException ex) { // notify user on failure // Note: A response is only sent for errors processing the action (which could be due to missing content // from the message). This is because any errors encountered prior represent a bad sender or ill-formed // message or token and thus represent a suspicious message. In that case we don't want to send a reply, for // security. messageReplier.reply(message, person, actionSelection, ex, inResponseMessages); throw ex; } } /** * Extracts the FROM address from the message. * * @param message * The message. * @return The FROM address. * @throws MessagingException * On error. */ String getFromAddress(final Message message) throws MessagingException { // insure the message has a From address Address[] addresses = message.getFrom(); if (addresses == null || addresses.length != 1 || addresses[0] == null) { throw new ValidationException("Message must contain a single From address."); } return ((InternetAddress) addresses[0]).getAddress(); } /** * Extracts the token from the message. * * @param message * The message. * @return The token. * @throws MessagingException * On error. */ String getToken(final Message message) throws MessagingException { // insure the message has a To address which 1) matches the expected system address, and 2) has an address tag Address[] addresses = message.getRecipients(RecipientType.TO); if (addresses != null) { boolean noReplyFound = false; for (int i = 0; i < addresses.length; i++) { String addr = ((InternetAddress) addresses[i]).getAddress(); // check for token-less system address: no-reply if (requiredToAddress.equals(addr)) { noReplyFound = true; } // check for token else if (addr.startsWith(toEmailRequiredStart) && addr.endsWith(toEmailRequiredEnd)) { String middle = addr.substring(toEmailRequiredStart.length(), addr.length() - toEmailRequiredEnd.length()); if (tokenEncoder.couldBeToken(middle)) { return middle; } } } if (noReplyFound) { return null; } } throw new ValidationException("Cannot find To address for the system with an address tag."); } }