/*
* Copyright (c) 2011-2012 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.action.execution.notification.notifier;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.app.event.implement.EscapeHtmlReference;
import org.apache.velocity.context.Context;
import org.eurekastreams.commons.exceptions.ExecutionException;
import org.eurekastreams.commons.server.UserActionRequest;
import org.eurekastreams.server.action.execution.email.NotificationEmailDTO;
import org.eurekastreams.server.action.execution.notification.NotificationPropertyKeys;
import org.eurekastreams.server.action.execution.notification.notifier.EmailNotificationTemplate.ReplyAction;
import org.eurekastreams.server.domain.HasEmail;
import org.eurekastreams.server.domain.NotificationType;
import org.eurekastreams.server.domain.stream.ActivityDTO;
import org.eurekastreams.server.search.modelview.PersonModelView;
import org.eurekastreams.server.service.actions.strategies.ActivityInteractionType;
import org.eurekastreams.server.service.email.TokenContentEmailAddressBuilder;
import org.eurekastreams.server.service.email.TokenContentFormatter;
import org.eurekastreams.server.service.utility.authorization.ActivityInteractionAuthorizationStrategy;
/**
* Notifier for in-app notifications. Builds the messages and stores them in the database.
*/
public class EmailNotifier implements Notifier
{
/** Apache Velocity templating engine. */
private final VelocityEngine velocityEngine;
/** Global context for Apache Velocity templating engine. (Holds system-wide properties.) */
private final Context velocityGlobalContext;
/** Message templates by notification type. */
private final Map<NotificationType, EmailNotificationTemplate> templates;
/** Prefix to use on email subjects. */
private final String subjectPrefix;
/** Builds the token content. */
private final TokenContentFormatter tokenContentFormatter;
/** Builds the recipient email address with a token. */
private final TokenContentEmailAddressBuilder tokenAddressBuilder;
/** For determining if users can comment on an activity. */
private final ActivityInteractionAuthorizationStrategy activityAuthorizer;
/** If HTML emails will be sent. (These will be multipart with a plain text component.) */
private final boolean sendHtml;
/**
* Constructor.
*
* @param inVelocityEngine
* Apache Velocity templating engine.
* @param inVelocityGlobalContext
* Global context for Apache Velocity templating engine.
* @param inTemplates
* Message templates by notification type.
* @param inSubjectPrefix
* Prefix to use on email subjects.
* @param inTokenContentFormatter
* Builds the token content.
* @param inTokenAddressBuilder
* Builds the recipient email address with a token.
* @param inActivityAuthorizer
* For determining if users can comment on an activity.
* @param inSendHtml
* If HTML emails will be sent. (These will be multipart with a plain text component.)
*/
public EmailNotifier(final VelocityEngine inVelocityEngine, final Context inVelocityGlobalContext,
final Map<NotificationType, EmailNotificationTemplate> inTemplates, final String inSubjectPrefix,
final TokenContentFormatter inTokenContentFormatter,
final TokenContentEmailAddressBuilder inTokenAddressBuilder,
final ActivityInteractionAuthorizationStrategy inActivityAuthorizer, final boolean inSendHtml)
{
velocityEngine = inVelocityEngine;
velocityGlobalContext = inVelocityGlobalContext;
templates = inTemplates;
subjectPrefix = inSubjectPrefix;
tokenContentFormatter = inTokenContentFormatter;
tokenAddressBuilder = inTokenAddressBuilder;
activityAuthorizer = inActivityAuthorizer;
sendHtml = inSendHtml;
}
/**
* {@inheritDoc}
*/
@Override
public Collection<UserActionRequest> notify(final NotificationType inType, final Collection<Long> inRecipients,
final Map<String, Object> inProperties, final Map<Long, PersonModelView> inRecipientIndex)
throws Exception
{
EmailNotificationTemplate template = templates.get(inType);
if (template == null)
{
// Not an error - this is an easy way to disable a given notification.
return null;
}
// prepare recipient lists
// determine which recipients get individual emails with reply tokens and which get a mass general email
List<String> addresses = new ArrayList<String>(inRecipients.size());
Map<String, String> addressesWithTokens = Collections.EMPTY_MAP;
if (template.getReplyAddressType() == ReplyAction.COMMENT)
{
Object obj = inProperties.get("activity");
if (obj == null || !(obj instanceof ActivityDTO))
{
throw new ExecutionException("Notification requires activity property for building token.");
}
ActivityDTO activity = (ActivityDTO) obj;
addressesWithTokens = new HashMap<String, String>(inRecipients.size());
String tokenData = tokenContentFormatter.buildForActivity(activity.getId());
// ok to use relaxed mode here: the translators wouldn't include recipients who do not have access to the
// activity
boolean generallyAllowed = activityAuthorizer.authorize(activity, ActivityInteractionType.COMMENT, false);
for (long recipientId : inRecipients)
{
String address = inRecipientIndex.get(recipientId).getEmail();
if (StringUtils.isNotBlank(address))
{
// Note: checking on a per-user basis is very inefficient. That's why the generallyAllowed
// optimization was put in to omit the per-user check for streams that allow commenting. Both the
// generallyAllowed and the per-user call could be replaced with an authorizer that takes a list of
// users to check and returns a list of only those which are allowed (i.e. an authorization filter).
// If the scenario arises where there are streams which do not allow commenting with many email
// subscribers, then a bulk authorizer could be used to significantly improve performance.
if (generallyAllowed
|| activityAuthorizer.authorize(recipientId, activity, ActivityInteractionType.COMMENT))
{
String replyAddress = tokenAddressBuilder.build(tokenData, recipientId);
addressesWithTokens.put(address, replyAddress);
}
else
{
addresses.add(address);
}
}
}
}
else
{
for (long recipientId : inRecipients)
{
String address = inRecipientIndex.get(recipientId).getEmail();
if (StringUtils.isNotBlank(address))
{
addresses.add(address);
}
}
}
int emailCount = addressesWithTokens.size() + (addresses.isEmpty() ? 0 : 1);
if (emailCount == 0)
{
return null;
}
List<UserActionRequest> requests = new ArrayList<UserActionRequest>(emailCount);
// -- prepare the email --
NotificationEmailDTO email = new NotificationEmailDTO();
// Note: The doubly-nested context is to prevent the code here and Velocity templates from updating
// inProperties. The inner context uses inProperties as its backing store, so anything added to the context --
// such as the values added here and any SET calls in templates -- would be added to inProperties, which we
// don't want.
Context velocityContext = new VelocityContext(new VelocityContext(inProperties, velocityGlobalContext));
velocityContext.put("context", velocityContext);
velocityContext.put("type", inType);
if (addressesWithTokens.size() + addresses.size() == 1)
{
velocityContext.put("recipient", inRecipientIndex.get(inRecipients.iterator().next()));
}
// build the subject
StringWriter writer = new StringWriter();
velocityEngine.evaluate(velocityContext, writer, "EmailSubject-" + inType, template.getSubjectTemplate());
email.setSubject(subjectPrefix + writer.toString());
// set the priority
email.setHighPriority(Boolean.TRUE.equals(inProperties.get(NotificationPropertyKeys.HIGH_PRIORITY)));
// render the body
String noReplyTextBody = null;
String replyTextBody = null;
String noReplyHtmlBody = null;
String replyHtmlBody = null;
// build the text body
Template vt = velocityEngine.getTemplate(template.getTextBodyTemplateResourcePath());
if (!addresses.isEmpty())
{
velocityContext.put("hasReplyAddress", false);
writer.getBuffer().setLength(0);
vt.merge(velocityContext, writer);
noReplyTextBody = writer.toString();
}
if (!addressesWithTokens.isEmpty())
{
velocityContext.put("hasReplyAddress", true);
writer.getBuffer().setLength(0);
vt.merge(velocityContext, writer);
replyTextBody = writer.toString();
}
// build the HTML body
if (sendHtml)
{
final String htmlBodyTemplateResourcePath = template.getHtmlBodyTemplateResourcePath();
if (htmlBodyTemplateResourcePath != null)
{
vt = velocityEngine.getTemplate(htmlBodyTemplateResourcePath);
// HTML-escape all content inserted
EventCartridge ec = new EventCartridge();
ec.addEventHandler(new EscapeHtmlReference());
ec.attachToContext(velocityContext);
if (!addresses.isEmpty())
{
velocityContext.put("hasReplyAddress", false);
writer.getBuffer().setLength(0);
vt.merge(velocityContext, writer);
noReplyHtmlBody = writer.toString();
}
if (!addressesWithTokens.isEmpty())
{
velocityContext.put("hasReplyAddress", true);
writer.getBuffer().setLength(0);
vt.merge(velocityContext, writer);
replyHtmlBody = writer.toString();
}
}
}
// -- create requests to send emails --
if (!addressesWithTokens.isEmpty())
{
email.setTextBody(replyTextBody);
if (replyHtmlBody != null)
{
email.setHtmlBody(replyHtmlBody);
}
for (Entry<String, String> entry : addressesWithTokens.entrySet())
{
NotificationEmailDTO userEmail = email.clone();
userEmail.setReplyTo(entry.getValue());
String address = entry.getKey();
userEmail.setToRecipient(address);
// set the description (for logging / debugging)
userEmail.setDescription(inType + " with token to " + address);
requests.add(new UserActionRequest("sendEmailNotificationAction", null, userEmail));
}
}
if (!addresses.isEmpty())
{
email.setTextBody(noReplyTextBody);
if (noReplyHtmlBody != null)
{
email.setHtmlBody(noReplyHtmlBody);
}
if (addresses.size() == 1)
{
final String address = addresses.get(0);
email.setToRecipient(address);
// set the description (for logging / debugging)
email.setDescription(inType + " to " + address);
}
else
{
email.setBccRecipients(StringUtils.join(addresses, ','));
// set the description (for logging / debugging)
email.setDescription(inType + " to " + inRecipients.size() + " recipients");
}
// set the reply-to to the actor (so replies to emails go to the actor, not the system)
if (template.getReplyAddressType() == ReplyAction.ACTOR)
{
Object obj = inProperties.get(NotificationPropertyKeys.ACTOR);
if (obj instanceof HasEmail)
{
HasEmail actor = (HasEmail) obj;
String actorEmail = actor.getEmail();
if (StringUtils.isNotBlank(actorEmail))
{
email.setReplyTo(actorEmail);
}
}
}
requests.add(new UserActionRequest("sendEmailNotificationAction", null, email));
}
return requests;
}
}