/**
* Copyright (C) 2011 JTalks.org Team
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jtalks.jcommune.service.nontransactional;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.tools.generic.EscapeTool;
import org.jtalks.common.model.entity.Entity;
import org.jtalks.jcommune.model.entity.*;
import org.jtalks.jcommune.service.dto.EntityToDtoConverter;
import org.jtalks.jcommune.service.exceptions.MailingFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.ui.velocity.VelocityEngineUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* This service is focused on sending e-mail to forum users.
* Notifications, confirmations or e-mail based subscriptions of a various
* kind should use this service to perform e-mail sending.
*
* @author Evgeniy Naumenko
* @author Eugeny Batov
*/
public class MailService {
public static final String REMOVE_TOPIC_SUBJECT_TEMPLATE = "removeTopic.subject";
public static final String REMOVE_CODE_REVIEW_SUBJECT_TEMPLATE = "removeCodeReview.subject";
public static final String REMOVE_TOPIC_MESSAGE_BODY_TEMPLATE = "removeTopic.vm";
public static final String REMOVE_CODE_REVIEW_MESSAGE_BODY_TEMPLATE = "removeCodeReview.vm";
private static final Logger LOGGER = LoggerFactory.getLogger(MailService.class);
private static final String LOG_TEMPLATE = "Error occurred while sending updates of %s %d to %s";
private static final String HTML_TEMPLATES_PATH = "org/jtalks/jcommune/service/templates/html/";
private static final String PLAIN_TEXT_TEMPLATES_PATH = "org/jtalks/jcommune/service/templates/plaintext/";
private static final String LINK = "link";
private static final String LINK_UNSUBSCRIBE = "link_unsubscribe";
private static final String LINK_LABEL = "linkLabel";
private static final String CUR_USER = "cur_user";
private static final String USER = "user";
private static final String NAME = "name";
private static final String TOPIC = "topic";
private static final String MESSAGE_SOURCE = "messageSource";
private static final String RECIPIENT_LOCALE = "locale";
private static final String NO_ARGS = "noArgs";
private static final String ESCAPE_TOOL = "escape";
private final JavaMailSender mailSender;
private final String from;
private final VelocityEngine velocityEngine;
private final MessageSource messageSource;
private final JCommuneProperty notificationsEnabledProperty;
private final EscapeTool escapeTool;
private final EntityToDtoConverter converter;
/**
* Creates a mailing service with a default template message autowired.
* "From" property is essential. Please note, that this address should be valid email address
* as most e-mail servers will reject e-mail if sender is not really correlated with
* the letter's "from" value.
*
* @param sender spring mailing tool
* @param from blank message with "from" filed preset
* @param engine engine for templating email notifications
* @param source for resolving internationalization messages
* @param notificationsEnabledProperty to check whether email notifications are enabled
* @param escapeTool velocity tool to perform html-escape
*/
public MailService(JavaMailSender sender,
String from,
VelocityEngine engine,
MessageSource source,
JCommuneProperty notificationsEnabledProperty,
EscapeTool escapeTool,
EntityToDtoConverter converter) {
this.mailSender = sender;
this.from = from;
this.velocityEngine = engine;
this.messageSource = source;
this.notificationsEnabledProperty = notificationsEnabledProperty;
this.escapeTool = escapeTool;
this.converter = converter;
}
/**
* Sends a password recovery message for the user.
* This method does not generate new password, just sends a message.
*
* @param user a person we will send a mail
* @param newPassword new user password to be placed in an email
* @throws MailingFailedException when mailing failed
*/
public void sendPasswordRecoveryMail(JCUser user, String newPassword) throws MailingFailedException {
String urlSuffix = "/login";
String url = this.getDeploymentRootUrl() + urlSuffix;
String name = user.getUsername();
Locale locale = user.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put(NAME, name);
model.put("newPassword", newPassword);
model.put(LINK, url);
model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix);
model.put(RECIPIENT_LOCALE, locale);
this.sendEmail(user.getEmail(), messageSource.getMessage("passwordRecovery.subject", new Object[]{}, locale),
model, "passwordRecovery.vm");
LOGGER.info("Password recovery email sent for {}", name);
}
/**
* Sends update notification to user specified if
* {@link SubscriptionAwareEntity} was updated, e.g. when some new
* information were added to the subscribed entity. This method won't check if user
* is subscribed to the particular notification or not.
*
* @param recipient a person to be notified about updates by email
* @param entity changed subscribed entity.
*/
public void sendUpdatesOnSubscription(JCUser recipient, SubscriptionAwareEntity entity) {
try {
String urlSuffix = entity.getUrlSuffix();
String url = this.getDeploymentRootUrl() + urlSuffix;
Locale locale = recipient.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put(LINK, url);
model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix);
model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl()
+ entity.getUnsubscribeLinkForSubscribersOf(entity.getClass()));
sendEmailOnForumUpdates(recipient, model, locale, (Entity) entity,
"subscriptionNotification.subject", "subscriptionNotification.vm");
} catch (MailingFailedException e) {
LOGGER.error(String.format(LOG_TEMPLATE,
entity.getClass().getCanonicalName(),
((Entity) entity).getId(),
recipient.getUsername()));
}
}
/**
* Sends email on forum updates.
*
* @param recipient a person to be notified about updates by email
* @param model template params to be substituted in velocity template
* @param locale recipient locale
* @throws MailingFailedException when mailing failed
*/
private void sendEmailOnForumUpdates(JCUser recipient, Map<String, Object> model, Locale locale,
Entity entity, String subject, String nameTemplate) throws MailingFailedException {
model.put(USER, recipient);
model.put(RECIPIENT_LOCALE, locale);
String titleEntity = this.getTitleName(entity);
this.sendEmail(recipient.getEmail(), messageSource.getMessage(subject,
new Object[]{}, locale) + titleEntity, model, nameTemplate);
}
/**
* Sends notification to user about received private message.
*
* @param recipient a person to be notified about received private message by email
* @param pm private message itself
*/
public void sendReceivedPrivateMessageNotification(JCUser recipient, PrivateMessage pm) {
try {
String urlSuffix = "/pm/inbox/" + pm.getId();
String url = this.getDeploymentRootUrl() + urlSuffix;
Locale locale = recipient.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put("recipient", recipient);
model.put(LINK, url);
model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix);
model.put(RECIPIENT_LOCALE, locale);
this.sendEmail(recipient.getEmail(),
messageSource.getMessage("receivedPrivateMessageNotification.subject", new Object[]{}, locale),
model, "receivedPrivateMessageNotification.vm");
} catch (MailingFailedException e) {
LOGGER.error(String.format(LOG_TEMPLATE, "Private message", pm.getId(), recipient.getUsername()));
}
}
/**
* Sends email with a hyperlink to activate user account.
*
* @param recipient user to send activation mail to
*/
public void sendAccountActivationMail(JCUser recipient) {
try {
String urlSuffix = "/user/activate/" + recipient.getUuid();
String url = this.getDeploymentRootUrl() + urlSuffix;
Locale locale = recipient.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put(NAME, recipient.getUsername());
model.put(LINK, url);
model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix);
model.put(RECIPIENT_LOCALE, locale);
this.sendEmail(recipient.getEmail(), messageSource.getMessage("accountActivation.subject",
new Object[]{}, locale), model, "accountActivation.vm");
} catch (MailingFailedException e) {
LOGGER.error("Failed to sent activation mail for user: " + recipient.getUsername());
}
}
/**
* Sends email to topic starter that his or her topic was moved
*
* @param recipient user to send notification
* @param topic relocated topic
* @param curUser User that moved topic
*/
public <T extends SubscriptionAwareEntity> void sendTopicMovedMail(
JCUser recipient, Topic topic, String curUser, Class<T> subsсriptionTargetClass) {
String urlSuffix = getTopicUrlSuffix(topic);
String url = this.getDeploymentRootUrl() + urlSuffix;
Locale locale = recipient.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put(NAME, recipient.getUsername());
model.put(CUR_USER, curUser);
model.put(LINK, url);
model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl()
+ topic.getUnsubscribeLinkForSubscribersOf(subsсriptionTargetClass));
model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix);
model.put(RECIPIENT_LOCALE, locale);
try {
this.sendEmail(recipient.getEmail(), messageSource.getMessage("moveTopic.subject",
new Object[]{}, locale), model, "moveTopic.vm");
} catch (MailingFailedException e) {
LOGGER.error("Failed to sent activation mail for user: " + recipient.getUsername());
}
}
/**
* Send email notification to user when he was mentioned in forum.
* Email notification will be sent only when notifications are enabled
* in forum, otherwise nothing will happen.
*
* @param recipient mentioned user who will receive notification
* @param postId id of post where user was mentioned
*/
public void sendUserMentionedNotification(JCUser recipient, long postId) {
String urlSuffix = "/posts/" + postId;
String url = this.getDeploymentRootUrl() + urlSuffix;
Locale locale = recipient.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put(NAME, recipient.getUsername());
model.put(LINK, url);
model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix);
model.put(RECIPIENT_LOCALE, locale);
try {
this.sendEmail(recipient.getEmail(), messageSource.getMessage("userMentioning.subject",
new Object[]{}, locale), model, "userMentioning.vm");
} catch (MailingFailedException e) {
LOGGER.error("Failed to sent activation mail for user: " + recipient.getUsername());
}
}
/**
* Just a convenience method for message sending to encapsulate
* boilerplate error handling code.
*
* @param to destination email address
* @param subject message headline
* @param model template params to be substituted in velocity template
* @param templateName template file name, like "template.vm"
* @throws MailingFailedException exception with error message specified ic case of some error
*/
private void sendEmail(String to, String subject, Map<String, Object> model,
String templateName) throws MailingFailedException {
if (!notificationsEnabledProperty.booleanValue()) {
LOGGER.debug("Email notifications are turned off in Forum Settings, skip sending to [{}]" +
" mail with subject [{}]. User with Admin Permissions can enter Poulpe (that should be changed" +
" soon) and change the setting.", to, subject);
return;
}
LOGGER.debug("Sending email to [{}] with subject [{}]", to, subject);
try {
model.put(MESSAGE_SOURCE, messageSource);
model.put(ESCAPE_TOOL, escapeTool);
model.put(NO_ARGS, new Object[]{});
String plainText = this.mergePlainTextTemplate(templateName, model);
String htmlText = this.mergeHtmlTemplate(templateName, model);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(to);
helper.setFrom(from);
helper.setSubject(subject);
helper.setText(plainText, htmlText);
mailSender.send(message);
} catch (MailException | MessagingException e) {
LOGGER.error("Mail sending failed", e);
throw new MailingFailedException(e);
}
}
/**
* Creates a html text message from templates and param given.
* Template should be located in org/jtalks/jcommune/service/templates/html/
*
* @param templateName template file name, like "template.vm"
* @param model template params to be substituted in velocity template
* @return html text message, ready to be sent
*/
private String mergeHtmlTemplate(String templateName, Map<String, Object> model) {
String path = HTML_TEMPLATES_PATH + templateName;
return VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, path, "UTF-8", model);
}
/**
* Creates a plain text message from templates and param given.
* Template should be located in org/jtalks/jcommune/service/templates/plaintext/
*
* @param templateName template file name, like "template.vm"
* @param model template params to be substituted in velocity template
* @return plain text message, ready to be sent
*/
private String mergePlainTextTemplate(String templateName, Map<String, Object> model) {
String path = PLAIN_TEXT_TEMPLATES_PATH + templateName;
return VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, path, "UTF-8", model);
}
/**
* @return current deployment root, e.g. "http://myhost.com:1234/mycoolforum"
*/
private String getDeploymentRootUrl() {
HttpServletRequest request = getServletRequest();
return request.getScheme()
+ "://" + request.getServerName()
+ ":" + request.getServerPort()
+ request.getContextPath();
}
/**
* Returns current deployment root without port for using as label link, for example.
*
* @return current deployment root without port, e.g. "http://myhost.com/mycoolforum"
*/
private String getDeploymentRootUrlWithoutPort() {
HttpServletRequest request = getServletRequest();
return request.getScheme()
+ "://" + request.getServerName()
+ request.getContextPath();
}
/**
* @return native {@link HttpServletRequest}
*/
private HttpServletRequest getServletRequest() {
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
return ((ServletRequestAttributes) attributes).getRequest();
}
/**
* @param entity entity like "Branch/Topics"
* @return title for Topic/Branch or "" if entity is not instanceof Topic/Branch
*/
private String getTitleName(Entity entity) {
if (entity instanceof Topic) {
Topic topic = (Topic) entity;
return ": " + topic.getTitle();
} else if (entity instanceof Branch) {
Branch branch = (Branch) entity;
return ": " + branch.getName();
} else if (entity instanceof Post) {
Post post = (Post) entity;
return ": " + post.getTopic().getTitle();
} else {
return "";
}
}
/**
* Set mail about removing topic.
*
* @param recipient Recipient for which send notification
* @param topic Current topic
* @param curUser User that removed the topic
*/
public void sendRemovingTopicMail(JCUser recipient, Topic topic, String curUser) {
Locale locale = recipient.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put(USER, recipient);
model.put(RECIPIENT_LOCALE, locale);
model.put(CUR_USER, curUser);
//Topic not exist more and user not subscribed to branch, so simply redirect to branch
model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + "/branches/" + topic.getBranch().getId());
model.put(TOPIC, topic);
try {
String subjectTemplate = REMOVE_TOPIC_SUBJECT_TEMPLATE;
String messageBodyTemplate = REMOVE_TOPIC_MESSAGE_BODY_TEMPLATE;
if (topic.isCodeReview()) {
subjectTemplate = REMOVE_CODE_REVIEW_SUBJECT_TEMPLATE;
messageBodyTemplate = REMOVE_CODE_REVIEW_MESSAGE_BODY_TEMPLATE;
}
String subject = messageSource.getMessage(subjectTemplate, new Object[]{}, locale);
this.sendEmail(recipient.getEmail(), subject, model, messageBodyTemplate);
} catch (MailingFailedException e) {
LOGGER.error("Failed to sent mail about removing topic or code review for user: "
+ recipient.getUsername());
}
}
/**
* Send email about new topic in the subscribed branch.
*
* @param subscriber recipient
* @param topic newly created topic
*/
void sendTopicCreationMail(JCUser subscriber, Topic topic) {
try {
String urlSuffix = getTopicUrlSuffix(topic);
String url = this.getDeploymentRootUrl() + urlSuffix;
Locale locale = subscriber.getLanguage().getLocale();
Map<String, Object> model = new HashMap<>();
model.put(LINK, url);
model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl()
+ topic.getBranch().getUnsubscribeLinkForSubscribersOf(Branch.class));
model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix);
sendEmailOnForumUpdates(subscriber, model, locale, topic.getBranch(),
"subscriptionNotification.subject", "branchSubscriptionNotification.vm");
} catch (MailingFailedException e) {
LOGGER.error("Failed to sent mail about creation topic for user: " + subscriber.getUsername());
}
}
/**
* Gets url suffix of specified topic. Urls of topics provided by plugins can differ
*
* @param topic topic to get url
*
* @return url of specified topic
*/
private String getTopicUrlSuffix(Topic topic) {
return converter.convertTopicToDto(topic).getTopicUrl();
}
}