package io.kaif.web;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import io.kaif.service.AccountService;
/**
* see http://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.html for how to handle aws sns
* request
* <p>
* <p>
* Created by ingram on 10/26/14.
*/
@RestController
@RequestMapping("/aws-sns")
public class AwsSnsRestController {
@JsonIgnoreProperties(ignoreUnknown = true)
static class SnsBody {
@JsonProperty("Type")
public String type;
@JsonProperty("MessageId")
public String messageId;
@JsonProperty("Message")
public String message;
@JsonProperty("SubscribeURL")
public String subscribeUrl;
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Delivery {
public List<String> recipients;
@Override
public String toString() {
return "Delivery{" +
"recipients=" + recipients +
'}';
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Complaint {
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ComplainedRecipient {
public String emailAddress;
@Override
public String toString() {
return "emailAddress='" + emailAddress + '\'';
}
}
public String complaintFeedbackType;
public List<ComplainedRecipient> complainedRecipients;
@Override
public String toString() {
return "Complaint{" +
"complaintFeedbackType='" + complaintFeedbackType + '\'' +
", complainedRecipients=" + complainedRecipients +
'}';
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Bounce {
@JsonIgnoreProperties(ignoreUnknown = true)
public static class BouncedRecipient {
public String emailAddress;
public String status;
public String action;
public String diagnosticCode;
@Override
public String toString() {
return "BouncedRecipient{" +
"emailAddress='" + emailAddress + '\'' +
", status='" + status + '\'' +
", action='" + action + '\'' +
", diagnosticCode='" + diagnosticCode + '\'' +
'}';
}
}
public String bounceType;
public String bounceSubType;
public List<BouncedRecipient> bouncedRecipients;
@Override
public String toString() {
return "Bounce{" +
"bounceType='" + bounceType + '\'' +
", bounceSubType='" + bounceSubType + '\'' +
", bouncedRecipients=" + bouncedRecipients +
'}';
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class MailNotification {
public String notificationType;
public Delivery delivery;
public Complaint complaint;
public Bounce bounce;
/**
* feedback type has 5 types, only 'not-spam' is positive
* <p>
* http://www.iana.org/assignments/marf-parameters/marf-parameters.xml#marf-parameters-2
*/
public List<String> badComplaintEmails() {
return Optional.ofNullable(complaint)
.filter(com -> !"not-spam".equalsIgnoreCase(com.complaintFeedbackType))
.map(com -> com.complainedRecipients)
.map(recs -> recs.stream()
.map((Complaint.ComplainedRecipient rec) -> rec.emailAddress)
.collect(Collectors.toList()))
.orElse(Collections.emptyList());
}
/**
* AWS document suggest remove bounceType=permanent
*/
public List<String> permanentBouncedEmails() {
return Optional.ofNullable(bounce)
.filter(com -> "Permanent".equalsIgnoreCase(com.bounceType))
.map(com -> com.bouncedRecipients)
.map(recs -> recs.stream()
.map((Bounce.BouncedRecipient rec) -> rec.emailAddress)
.collect(Collectors.toList()))
.orElse(Collections.emptyList());
}
}
private static final Logger logger = Logger.getLogger(AwsSnsRestController.class);
private static final String NO_SUCH_FIELD = "-no-such-field";
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private AccountService accountService;
public AwsSnsRestController() {
//spring
}
@VisibleForTesting
AwsSnsRestController(AccountService accountService) {
this.accountService = accountService;
}
/**
* note that this url address <code>https://kaif.io/aws-sns/mail-feedback</code> is
* used
* as subscribe endpoint of topic 'kaif-mail-feedback' in
* AWS SNS
*/
@RequestMapping(value = "/mail-feedback", method = RequestMethod.POST)
public void mailFeedback(HttpServletRequest request) throws IOException {
String messageType = Optional.ofNullable(request.getHeader("x-amz-sns-message-type"))
.map(Strings::emptyToNull)
.orElse(NO_SUCH_FIELD);
switch (messageType) {
case "SubscriptionConfirmation":
handleSubscriptionConfirmation(request);
break;
case "Notification":
handleNotification(request);
break;
default:
logger.warn("receive unknown sns, headers:\n"
+ dumpHeaders(request)
+ "\nbody:\n"
+ inputToString(request.getInputStream()));
break;
}
}
private String inputToString(InputStream in) throws IOException {
return new String(ByteStreams.toByteArray(in), "UTF-8");
}
private void handleNotification(HttpServletRequest request) throws IOException {
String body = inputToString(request.getInputStream());
logger.debug("processing Notification... \nheaders:\n"
+ dumpHeaders(request)
+ "\nbody:\n"
+ body);
SnsBody snsBody = objectMapper.readValue(body, SnsBody.class);
MailNotification mailNotification = objectMapper.readValue(snsBody.message,
MailNotification.class);
String notificationType = Optional.ofNullable(mailNotification.notificationType)
.map(Strings::emptyToNull)
.orElseThrow(() -> new IllegalArgumentException("missing notificationType field"));
switch (notificationType) {
case "Bounce":
accountService.muteEmail(mailNotification.permanentBouncedEmails());
logger.info("receive... " + mailNotification.bounce);
break;
case "Complaint":
accountService.complaintEmail(mailNotification.badComplaintEmails());
logger.info("receive... " + mailNotification.complaint);
break;
case "Delivery":
logger.debug("receive... " + mailNotification.delivery);
break;
default:
logger.warn("receive unknown notificationType, ignore");
break;
}
}
private String dumpHeaders(HttpServletRequest request) {
return toList(request.getHeaderNames()).stream()
.collect(Collectors.toMap(name -> name, name -> toList(request.getHeaders(name))))
.toString();
}
private List<String> toList(Enumeration<String> headerNames) {
List<String> names = new ArrayList<>();
while (headerNames.hasMoreElements()) {
names.add(headerNames.nextElement());
}
return names;
}
private void handleSubscriptionConfirmation(HttpServletRequest request) throws IOException {
String body = inputToString(request.getInputStream());
logger.info("processing SubscriptionConfirmation... \nheaders:\n"
+ dumpHeaders(request)
+ "\nbody:\n"
+ body);
SnsBody snsBody = objectMapper.readValue(body, SnsBody.class);
String url = Optional.ofNullable(snsBody.subscribeUrl)
.map(Strings::emptyToNull)
.orElseThrow(() -> new IllegalArgumentException("missing SubscribeURL field"));
int statusCode = requestUrl(url);
if (statusCode >= 400) {
logger.error("confirm subscribe failed:" + url + ", statusCode:" + statusCode);
throw new IOException("subscribe failed");
} else {
logger.info("SubscriptionConfirmation done: " + url);
}
}
private int requestUrl(String url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
return connection.getResponseCode();
}
}