/*
* Copyright 2012 Nodeable Inc
*
* 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 com.streamreduce.core.service;
import com.amazonaws.AmazonClientException;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;
import com.amazonaws.services.simpleemail.model.Body;
import com.amazonaws.services.simpleemail.model.Content;
import com.amazonaws.services.simpleemail.model.Destination;
import com.amazonaws.services.simpleemail.model.Message;
import com.amazonaws.services.simpleemail.model.SendEmailRequest;
import com.amazonaws.services.simpleemail.model.SendEmailResult;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.streamreduce.core.model.Account;
import com.streamreduce.core.model.Connection;
import com.streamreduce.core.model.User;
import com.streamreduce.core.model.messages.MessageComment;
import com.streamreduce.core.model.messages.MessageType;
import com.streamreduce.core.model.messages.SobaMessage;
import com.streamreduce.core.model.messages.details.AbstractMessageDetails;
import com.streamreduce.core.service.exception.UserNotFoundException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import javax.annotation.Resource;
import net.sf.json.JSONObject;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service("emailService")
public class EmailServiceImpl extends AbstractService implements EmailService, InitializingBean {
private static final String DEFAULT_ENCODING = "UTF-8";
@Resource(name = "emailProperties")
protected Properties emailProperties;
@Resource(name = "simpleEmailServiceClient")
protected AmazonSimpleEmailServiceClient simpleEmailServiceClient;
@Resource(name = "userService")
protected UserService userService;
@Value("${email.enabled}")
protected boolean enabled;
/*
* This is basically an undocumented function to allow unit testing to continue when false.
*/
@Value("${email.background.send:true}")
protected boolean backgroundSend = true;
private ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
@Override
public void afterPropertiesSet() throws Exception {
//Load templates from classpath for Velocity and cache them
Velocity.setProperty(Velocity.RESOURCE_LOADER, "class");
Velocity.setProperty("class.resource.loader.class", ClasspathResourceLoader.class.getName());
Velocity.setProperty("class.resource.loader.cache", true);
//Use log4j for logging in Velocity (otherwise, it uses its own)
Velocity.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.Log4JLogChute");
Velocity.setProperty("runtime.log.logsystem.log4j.logger", logger.getName());
Velocity.init();
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public void sendPasswordResetEmail(User user, boolean mobile) {
try {
String userId = URLEncoder.encode(user.getId().toString(), DEFAULT_ENCODING);
String key = URLEncoder.encode(user.getSecretKey(), DEFAULT_ENCODING);
String secretKey = key + "/" + userId;
String from = (String) emailProperties.get("email.from");
String urlPrefix = mobile ? (String) emailProperties.get("email.mobile.urlprefix") : (String) emailProperties.get("email.urlprefix");
String subject = (String) emailProperties.get("email.resetPassword.subject");
VelocityContext context = new VelocityContext();
context.put("urlPrefix", urlPrefix);
context.put("secretKey", secretKey);
logger.debug("[SES EMAIL] sending password reset email for " + user.getUsername());
// send the email
sendEmail(user.getUsername(), from, subject, "password_reset_email", context);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
@Override
public void sendNewUserActivationEmail(User user) {
try {
// body
String id = URLEncoder.encode(user.getId().toString(), DEFAULT_ENCODING);
String key = URLEncoder.encode(user.getSecretKey(), DEFAULT_ENCODING);
// append to the url
String secretyKey = key + "/" + id;
String from = (String) emailProperties.get("email.from");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
String subject = (String) emailProperties.get("email.newUser.subject");
VelocityContext context = new VelocityContext();
context.put("urlPrefix", urlPrefix);
context.put("secretKey", secretyKey);
logger.debug("[SES EMAIL] send new user activation email for " + user.getUsername());
// send the email
sendEmail(user.getUsername(), from, subject, "new_user_activation_email", context);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
@Override
public void sendInviteUserActivationEmail(User user) {
try {
String id = URLEncoder.encode(user.getAccount().getId().toString(), DEFAULT_ENCODING);
String key = URLEncoder.encode(user.getSecretKey(), DEFAULT_ENCODING);
String secretyKey = key + "/" + id;
String from = (String) emailProperties.get("email.from");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
String subject = (String) emailProperties.get("email.inviteUser.subject");
VelocityContext context = new VelocityContext();
context.put("urlPrefix", urlPrefix);
context.put("secretKey", secretyKey);
logger.debug("[SES EMAIL] send invited user activation email for " + user.getUsername());
sendEmail(user.getUsername(), from, subject, "invite_user_activation_email", context);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
@Override
public void sendUserAccountSetupCompleteEmail(User user) {
String from = (String) emailProperties.get("email.from");
String subject = (String) emailProperties.get("email.userSetup.subject");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
VelocityContext context = new VelocityContext();
context.put("urlPrefix", urlPrefix);
logger.debug("[SES EMAIL] send account setup complete email for " + user.getUsername());
sendEmail(user.getUsername(), from, subject, "user_account_setup_complete_email", context);
}
@Override
public void sendInsightsAvailableEmail(List<User> users) {
String from = (String) emailProperties.get("email.from");
String subject = (String) emailProperties.get("email.accountInsight.subject");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
VelocityContext context = new VelocityContext();
context.put("urlPrefix", urlPrefix);
for (User user : users) {
logger.debug("[SES EMAIL] send new account insights are available email for " + user.getUsername());
sendEmail(user.getUsername(), from, subject, "new_account_insight_available", context);
}
}
@Override
public void sendBugReport(String username, String company, String summary, String details, String debugInfo) {
// from support
String to = (String) emailProperties.get("email.support.from");
String subject = "[BugTracker] " + summary;
VelocityContext velocityContext = new VelocityContext();
velocityContext.put("uuid", UUID.randomUUID().toString());
velocityContext.put("dateCreated", new Date().toString());
velocityContext.put("company", company);
velocityContext.put("username", username);
velocityContext.put("summary", summary);
velocityContext.put("details", details);
velocityContext.put("debugInfo", debugInfo);
sendEmail(to, username, subject, "bug_report_email", velocityContext);
}
// @Override
// public void sendDirectMessageEmail(List<User> userList, SobaMessage sobaMessage) {
//
// String from = (String) emailProperties.get("email.from");
// String subject = "Nodable: direct message test";
//
// VelocityContext velocityContext = new VelocityContext();
// velocityContext.put("sobaMessage", sobaMessage);
//
// String body = createBodyFromTemplate("/templates/direct_message_email_html.vm", velocityContext);
//
// logger.debug("[SES EMAIL] send direct message notification");
//
// for (User user : userList) {
// // don't send a copy to the user who created it.
// if (!sobaMessage.getSenderId().equals(user.getId())) {
// sendEmail(user.getUsername(), from, subject, body);
// }
// }
// }
@Override
public void sendCommentAddedEmail(Account account, SobaMessage sobaMessage, MessageComment comment) {
User owner = null;
if (sobaMessage.getOwnerId() != null) {
try {
owner = userService.getUserById(sobaMessage.getOwnerId());
}
catch (UserNotFoundException e) {
logger.error("User not found for ID {}. Cannot send email.", sobaMessage.getOwnerId());
return;
}
}
String from = (String) emailProperties.get("email.noreply.from");
String subject = (String) emailProperties.get("email.comment.added.subject");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
// try to use a sensible default if the details or title are null
String title = "User Comment";
if (sobaMessage.getDetails() != null) {
AbstractMessageDetails details = (AbstractMessageDetails) sobaMessage.getDetails();
if (details.getTitle() != null) {
title = details.getTitle();
}
}
VelocityContext context = new VelocityContext();
context.put("messageId", sobaMessage.getId());
context.put("commenter", comment.getFullName());
context.put("messageType", getFriendlyMessageTypeLabel(sobaMessage.getType()));
context.put("urlPrefix", urlPrefix);
context.put("title", title);
context.put("comment", comment.getComment());
List<User> enabledUsers = userService.allEnabledUsersForAccount(account);
for (User user : enabledUsers) {
// don't send if the commenter is the owner or if the user has elected to not receive notifications
if (owner == null || !user.getId().equals(owner.getId()) || !receivesCommentNotifications(user)) {
sendEmail(user.getUsername(), from, subject, "comment_added", context);
}
}
}
@Override
public void sendUserMessageAddedEmail(User sender, SobaMessage sobaMessage) {
String from = (String) emailProperties.get("email.noreply.from");
String subject = (String) emailProperties.get("email.user.message.added.subject");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
VelocityContext context = new VelocityContext();
context.put("sender", sender.getFullname());
context.put("messageId", sobaMessage.getId());
context.put("messageType", getFriendlyMessageTypeLabel(sobaMessage.getType()));
context.put("urlPrefix", urlPrefix);
context.put("message", sobaMessage.getTransformedMessage());
List<User> enabledUsers = userService.allEnabledUsersForAccount(sender.getAccount());
for (User user : enabledUsers) {
// don't send if the sender is the owner or if the user has elected to not receive notifications
if (!user.getId().equals(sender.getId()) && receivesNewMessageNotifications(user)) {
sendEmail(user.getUsername(), from, subject, "user_message_added", context);
}
}
}
@Override
public void sendUserMessageEmail(User sender, SobaMessage sobaMessage, JSONObject payload) {
String from = (String) emailProperties.get("email.noreply.from");
String subject = (String) emailProperties.get("email.user.message.subject");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
VelocityContext context = new VelocityContext();
context.put("sender", sender.getFullname());
context.put("messageId", sobaMessage.getId());
context.put("messageType", getFriendlyMessageTypeLabel(sobaMessage.getType()));
context.put("urlPrefix", urlPrefix);
context.put("message", payload.getString("body"));
// the payload may have a custom subject line
if (payload.containsKey("subject")) {
subject = payload.getString("subject");
}
String recipients = payload.getString("recipient");
for (String recipient : recipients.split(",")) {
sendEmail(recipient, from, subject, "user_message", context);
}
}
@Override
public void sendConnectionBrokenEmail(Connection connection) {
String from = (String) emailProperties.get("email.noreply.from");
String subject = (String) emailProperties.get("email.connection.error.subject");
String urlPrefix = (String) emailProperties.get("email.urlprefix");
User accountAdmin = userService.getAccountAdmin(connection.getAccount());
VelocityContext context = new VelocityContext();
context.put("urlPrefix", urlPrefix);
context.put("connectionName", connection.getAlias());
context.put("connectionId", connection.getId());
sendEmail(accountAdmin.getUsername(), from, subject, "connection_error", context);
}
/**
* Sends an email using the default Sender Name.
*
* @param recipients recipient email addresses
* @param fromAddress the email address we are sending the email from (
* The full "Sender Name <sender@host.com>" defaults to the email.from.name property)
* @param subject Subject Line, also passed through Velocity
* @param templateName the base Velocity template. The method will look for "/templates/[templateName]_html.vm" and "/templates/[templateName]_text.vm"
* @param context Velocity context used to populate the templates
*/
protected void sendEmail(Collection<String> recipients, String fromAddress, String subject, String templateName, VelocityContext context) {
String htmlBody = createBodyFromTemplate("/templates/"+templateName+"_html.vm", context);
String textBody = createBodyFromTemplate("/templates/"+templateName+"_text.vm", context);
String templateSubject = createStringFromTemplate(subject, context);
String senderName = (String) emailProperties.get("email.from.name");
String senderNameWithAddress = senderName == null ? fromAddress :
senderName + "<" + fromAddress + ">";
for (String recipient : recipients) {
sendEmail(recipient, senderNameWithAddress, fromAddress, templateSubject, htmlBody, textBody);
}
}
protected void sendEmail(String recipient, String fromAddress, String subject, String templateName, VelocityContext context) {
Set<String> recipients = Sets.newHashSet(recipient);
sendEmail(recipients, fromAddress, subject, templateName, context);
}
/**
* Sends an email.
*
* @param to the address to send the email to.
* @param from A String email address or a String in the form of "Name <email_address>"
* @param replyTo the email address to replyTO
* @param subject Subject Line
* @param htmlBody Message Body
*/
protected void sendEmail(final String to, final String from, final String replyTo, final String subject, final String htmlBody, final String textBody) {
if (!enabled) {
logger.debug("EmailService is currently disabled so email not sent");
return;
}
if (backgroundSend) {
ListenableFuture<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return internalSendEmail(to, from, replyTo, subject, htmlBody, textBody);
}
});
// We don't really need this, but it's stubbed out for now.
Futures.addCallback(future, new FutureCallback<String>() {
@Override
public void onSuccess(String s) { }
@Override
public void onFailure(Throwable throwable) { }
});
}
else {
internalSendEmail(to, from, replyTo, subject, htmlBody, textBody);
}
}
private String internalSendEmail(String to, String from, String replyTo, String subject, String htmlBody, String textBody) {
SendEmailRequest request = new SendEmailRequest().
withSource(from).
withReplyToAddresses(replyTo);
List<String> toAddresses = new ArrayList<>();
toAddresses.add(to);
Destination dest = new Destination().withToAddresses(toAddresses);
request.setDestination(dest);
Content subjContent = new Content().withData(subject);
Message msg = new Message().withSubject(subjContent);
// Include a body in both text and HTML formats
Body theBody = new Body();
if (textBody != null) {
theBody.withText(new Content().withData(textBody));
}
if (htmlBody != null) {
theBody.withHtml(new Content().withData(htmlBody));
}
msg.setBody(theBody);
request.setMessage(msg);
// Call Amazon SES to send the message
String messageId = null;
try {
SendEmailResult result = simpleEmailServiceClient.sendEmail(request);
messageId = result.getMessageId();
} catch (AmazonClientException e) {
logger.debug("[SES EMAIL] AWS AmazonClientException " + e.getMessage());
logger.error("Caught an AddressException, which means one or more of your "
+ "addresses are improperly formatted." + e.getMessage());
} catch (Exception e) {
logger.debug("[SES EMAIL] AWS General Exception " + e.getMessage());
}
return messageId;
}
private String createBodyFromTemplate(String templateLocation, VelocityContext context) {
Template template = Velocity.getTemplate(templateLocation);
template.setEncoding(DEFAULT_ENCODING);
StringWriter body = new StringWriter();
template.merge(context, body);
return body.toString();
}
private String createStringFromTemplate(String stringTemplate, VelocityContext context) {
StringWriter body = new StringWriter();
Velocity.evaluate(context, body, "SUBJECT", stringTemplate);
return body.toString();
}
private String getFriendlyMessageTypeLabel(MessageType type) {
switch (type) {
case ACTIVITY:
return "activity";
case AGENT:
return "agent";
case SYSTEM:
return "system";
case USER:
return "user";
case CONNECTION:
return "connection";
case INVENTORY_ITEM:
return "inventory";
case GATEWAY:
return "gateway";
case NODEBELLY:
return "insight";
default: return "";
}
}
private boolean receivesCommentNotifications(User user) {
return Boolean.valueOf(user.getConfig().get(User.ConfigKeys.RECEIVES_COMMENT_NOTIFICATIONS).toString());
}
private boolean receivesNewMessageNotifications(User user) {
return Boolean.valueOf(user.getConfig().get(User.ConfigKeys.RECEIVES_NEW_MESSAGE_NOTIFICATIONS).toString());
}
}