/*
* Copyright 2011 Michele Mancioppi [michele.mancioppi@gmail.com]
*
* 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 cave.nice.testMessage.mail;
import java.io.IOException;
import java.util.Date;
import java.util.Properties;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import cave.nice.testMessage.TestMessageConstants;
import cave.nice.testMessage.data.CannotUpdateEntityException;
import cave.nice.testMessage.data.DataManager;
import cave.nice.testMessage.data.DataManagerException;
import cave.nice.testMessage.data.Test;
import cave.nice.testMessage.data.TestAlreadyAnsweredException;
import cave.nice.testMessage.data.UknownTestChallengeException;
import cave.nice.testMessage.data.UnknownVerifiedAccountException;
import cave.nice.testMessage.data.UnprocessedEMail;
import cave.nice.testMessage.data.VerifiedAccount;
import cave.nice.testMessage.data.WrongAccountForTestException;
import com.google.appengine.api.datastore.KeyFactory;
/**
* Processes the incoming messages. They are supposed to be test answers. If
* not, make a fuss.
*
* @author Michele
*/
@SuppressWarnings("serial")
public class MailHandlerServlet extends HttpServlet {
private static final Logger LOGGER = Logger
.getLogger(MailHandlerServlet.class.getName());
private InternetAddress appInternetAddress = null;
@Override
public void init(ServletConfig config) throws ServletException {
try {
appInternetAddress = new InternetAddress(
TestMessageConstants.TESTS_EMAIL_ADDRESS, true);
} catch (AddressException e) {
throw new ServletException(
"Error while creating the app internet address using the email '"
+ TestMessageConstants.TESTS_EMAIL_ADDRESS + "'", e);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
DataManager dataStoreManager = DataManager.getInstance();
try {
Properties props = new Properties();
Session session = Session.getDefaultInstance(props, null);
MimeMessage message = null;
try {
message = new MimeMessage(session, req.getInputStream());
} catch (MessagingException e) {
throw new ServletException(
"Error while retrieving an incoming mail", e);
}
Address[] froms = null;
try {
froms = message.getFrom();
} catch (MessagingException e) {
throw new ServletException(
"Error while retrieving an the 'from' field from the following message: "
+ message, e);
}
InternetAddress from = null;
switch (froms.length) {
case 0: {
/*
* No from address? How are we supposed to check the validity of
* the test?
*
* TODO Check if the email has at least a valid test identifier
* inside. If not, figure out something
*/
throw new ServletException(
"The following message has no 'from' field set: "
+ message);
}
case 1: {
/*
* That's how we like it ;-)
*/
from = (InternetAddress) froms[0];
break;
}
default: {
/*
* Uhm... multiple 'from'? TODO Check for a 'from' we know, and
* assume that. Plus, log a warning.
*/
throw new ServletException(
"The following message has multiple 'from' fields set: "
+ message);
}
}
try {
if (message
.getHeader(TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER) != null
|| message
.getHeader(TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER) != null) {
processTestAnswerInHeaders(dataStoreManager, message);
} else {
processTestAnswerInMessageBody(dataStoreManager, message,
from);
}
} catch (DataManagerException e) {
throw new ServletException(e.getMessage());
} catch (Exception e) {
UnprocessedEMail em = dataStoreManager
.storeUnprocessedEMail(message);
LOGGER.log(Level.SEVERE,
"Incoming message could not be processed, saved as UnprocessedEMail "
+ KeyFactory.keyToString(em.getIdentifier()), e);
}
} finally {
dataStoreManager.close();
}
}
private void processTestAnswerInHeaders(DataManager dataManager,
MimeMessage message) throws ServletException,
UknownTestChallengeException, UnknownVerifiedAccountException,
WrongAccountForTestException, CannotUpdateEntityException {
try {
String[] testIdentifiers = message
.getHeader(TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER);
if (testIdentifiers.length == 0) {
throw new ServletException("Required SMTP header '"
+ TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER
+ "' not found");
} else if (testIdentifiers.length > 1) {
throw new ServletException(
"More than one value for the header '"
+ TestMessageConstants.SMTP_HEADER_TEST_IDENTIFIER
+ "' found");
}
String[] accountIdentifiers = message
.getHeader(TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER);
if (accountIdentifiers.length == 0) {
throw new ServletException("Required SMTP header '"
+ TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER
+ "' not found");
} else if (accountIdentifiers.length > 1) {
throw new ServletException(
"More than one value for the header '"
+ TestMessageConstants.SMTP_HEADER_ACCOUNT_IDENTIFIER
+ "' found");
}
String testChallenge = testIdentifiers[0];
UUID challenge = UUID.fromString(testChallenge);
String accountIdentifier = accountIdentifiers[0];
VerifiedAccount account = dataManager.getVerifiedAccount(KeyFactory
.stringToKey(accountIdentifier));
processSMTPTestAnswer(dataManager, account, challenge);
} catch (MessagingException e) {
throw new ServletException(
"Error while accessing the headers of the message", e);
} catch (TestAlreadyAnsweredException e) {
/*
* TODO Error message
*/
throw new ServletException(e);
}
}
private void processTestAnswerInMessageBody(DataManager dataStoreManager,
MimeMessage message, InternetAddress from) throws ServletException,
IOException, UnknownVerifiedAccountException,
WrongAccountForTestException, CannotUpdateEntityException,
UknownTestChallengeException, TestAlreadyAnsweredException {
/*
* It could be that the address is the one used to send the test in the
* first place :D
*/
VerifiedAccount account = null;
if (from.equals(appInternetAddress)) {
/*
* OK, they just forwarded it.
*/
Address[] tos = null;
try {
tos = message.getRecipients(RecipientType.TO);
} catch (MessagingException e) {
throw new ServletException(
"Cannot access TO's of the message: " + message);
}
if (tos != null && tos.length > 0) {
for (Address to : tos) {
if (!(to instanceof InternetAddress)) {
/*
* Well, if this is the case, we want nothing to do with
* this address :D
*/
continue;
}
try {
account = dataStoreManager
.getVerifiedAccount((InternetAddress) to);
break;
} catch (UnknownVerifiedAccountException e) {
/*
* We don't know this. Strange, but let's move on
*/
}
}
}
if (account == null) {
throw new ServletException("Message to unknown accounts '"
+ tos + "': " + message);
}
} else {
/*
* It is not a forward... Could it be a reply?
*/
try {
account = dataStoreManager.getVerifiedAccount(from);
} catch (UnknownVerifiedAccountException e) {
throw new ServletException("Message from unknown account '"
+ from + "': " + message);
}
}
/*
* OK, we know the account. Now, do we recognize the test that is
* answered?
*/
String body = null;
try {
body = IOUtils.toString(message.getInputStream());
body = body.replaceAll("\\s", "");
} catch (MessagingException e) {
throw new ServletException("Cannot access content of the message:"
+ message);
}
/*
* Remove from body the white spaces: if one has been inserted in the
* id, we would not recognize it
*/
Matcher matcher = null;
String pattern = ".*#####(.+)#####.*";
try {
matcher = Pattern.compile(pattern).matcher(body);
} catch (PatternSyntaxException e) {
throw new ServletException("Cannot compile pattern: " + pattern, e);
}
if (!matcher.matches()) {
throw new ServletException("No test identifier found in the body: "
+ body);
}
String testChallenge = matcher.group(1);
UUID challenge = UUID.fromString(testChallenge);
processSMTPTestAnswer(dataStoreManager, account, challenge);
}
private void processSMTPTestAnswer(DataManager dataStoreManager,
VerifiedAccount account, UUID testChallenge)
throws UknownTestChallengeException,
UnknownVerifiedAccountException, WrongAccountForTestException,
CannotUpdateEntityException, TestAlreadyAnsweredException {
Test openTest = dataStoreManager.getTestOfAccount(account,
testChallenge);
LOGGER.info("SMTP answer to the test '"
+ KeyFactory.keyToString(openTest.getIdentifier())
+ "' of the email account '"
+ openTest.getAccount().getEmailAddress() + "'");
dataStoreManager.markTestAsAnswered(openTest, new Date());
}
}