/*
* Copyright 2014-2016 CyberVision, 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 org.kaaproject.kaa.server.admin.services.messaging;
import com.google.common.net.UrlEscapers;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.kaaproject.kaa.server.admin.services.dao.PropertiesFacade;
import org.kaaproject.kaa.server.admin.services.entity.gen.GeneralProperties;
import org.kaaproject.kaa.server.admin.services.entity.gen.SmtpMailProperties;
import org.kaaproject.kaa.server.admin.shared.util.UrlParams;
import org.kaaproject.kaa.server.admin.shared.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
@Service("messagingService")
public class MessagingServiceImpl implements MessagingService {
private static final Logger LOG = LoggerFactory.getLogger(MessagingServiceImpl.class);
@Autowired
private PropertiesFacade propertiesFacade;
@Autowired
private MessageSource messages;
private JavaMailSenderImpl kaaMessagingMailSender;
private ExecutorService sendPool;
private int sendPoolSize;
private int sendTimeout;
@Value("#{'http://' + properties[transport_public_interface] + ':' + properties[admin_port]}")
private String appBaseUrl;
private String appName;
private String mailFrom;
public MessagingServiceImpl() {
}
public void setSendPoolSize(int sendPoolSize) {
this.sendPoolSize = sendPoolSize;
}
public void setSendTimeout(int sendTimeout) {
this.sendTimeout = sendTimeout;
}
/**
* Initialize messaging service. Call after bean creation (bean init-method).
*/
public void init() {
String sendName = "send-message-call-runner-%d";
sendPool = Executors.newFixedThreadPool(
sendPoolSize, new ThreadFactoryBuilder().setNameFormat(sendName).build());
configureMailSender();
}
/**
* Bean destroy-method. Call before bean destroying.
*/
public void destroy() {
if (sendPool != null) {
sendPool.shutdown();
try {
while (sendPool.isTerminated() == false) {
sendPool.awaitTermination(sendTimeout, TimeUnit.SECONDS);
}
} catch (InterruptedException ex) {
LOG.warn("shutdown interrupted on {}", sendPool, ex);
}
}
}
@Override
public void configureMailSender() {
SmtpMailProperties smtpMailProperties = propertiesFacade.getSpecificProperties(
SmtpMailProperties.class);
kaaMessagingMailSender = new JavaMailSenderImpl();
kaaMessagingMailSender.setHost(smtpMailProperties.getSmtpHost());
kaaMessagingMailSender.setPort(smtpMailProperties.getSmtpPort());
kaaMessagingMailSender.setUsername(smtpMailProperties.getUsername());
kaaMessagingMailSender.setPassword(smtpMailProperties.getPassword());
Properties javaMailProperties = toJavaMailProperties(smtpMailProperties);
kaaMessagingMailSender.setJavaMailProperties(javaMailProperties);
mailFrom = smtpMailProperties.getMailFrom();
GeneralProperties generalProperties = propertiesFacade.getSpecificProperties(
GeneralProperties.class);
// appBaseUrl = generalProperties.getBaseUrl(); getBaseUrl return always localhost:8080 and
// retrieve this data from DB instead of kaa-node property file ; TODO(KAA-1619) refactor GeneralProperties
appName = generalProperties.getAppTitle();
}
private Properties toJavaMailProperties(SmtpMailProperties smtpMailProperties) {
Properties javaMailProperties = new Properties();
String protocol = smtpMailProperties.getSmtpProtocol().toString().toLowerCase();
javaMailProperties.put("mail.transport.protocol", protocol);
javaMailProperties.put("mail."
+ protocol + ".host", smtpMailProperties.getSmtpHost());
javaMailProperties.put("mail."
+ protocol + ".port", String.valueOf(smtpMailProperties.getSmtpPort()));
javaMailProperties.put("mail."
+ protocol + ".timeout", String.valueOf(smtpMailProperties.getTimeout()));
javaMailProperties.put("mail."
+ protocol + ".auth", String.valueOf(!Utils.isEmpty(smtpMailProperties.getUsername())));
javaMailProperties.put("mail."
+ protocol + ".starttls.enable", String.valueOf(smtpMailProperties.getEnableTls()));
javaMailProperties.put("mail.debug", "true");
return javaMailProperties;
}
@Override
public void sendTempPassword(final String username,
final String password,
final String email) throws Exception {
String subject = messages.getMessage(
"tempPasswordMailMessageSubject", new Object[]{appName}, Locale.ENGLISH);
String text = messages.getMessage(
"tempPasswordMailMessageBody",
new Object[]{appBaseUrl, appName, username, password},
Locale.ENGLISH);
MimeMessage mimeMsg = kaaMessagingMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, "UTF-8");
helper.setFrom(mailFrom);
helper.setTo(email);
helper.setSubject(subject);
helper.setText(text, true);
kaaMessagingMailSender.send(helper.getMimeMessage());
}
@Override
public void sendPasswordResetLink(final String passwordResetHash,
final String username,
final String email) {
try {
callAsync(new Callable<Void>() {
@Override
public Void call() throws Exception {
Map<String, String> paramsMap = new HashMap<>();
paramsMap.put(UrlParams.RESET_PASSWORD, passwordResetHash);
String params = "#" + generateParamsUrl(paramsMap);
String subject = messages.getMessage(
"resetPasswordMailMessageSubject", new Object[]{appName}, Locale.ENGLISH);
String text = messages.getMessage(
"resetPasswordMailMessageBody",
new Object[]{username, appName, appBaseUrl + params},
Locale.ENGLISH);
MimeMessage mimeMsg = kaaMessagingMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, "UTF-8");
try {
helper.setFrom(mailFrom);
helper.setTo(email);
helper.setSubject(subject);
helper.setText(text, true);
kaaMessagingMailSender.send(helper.getMimeMessage());
} catch (MessagingException ex) {
LOG.error("Unexpected error while sendPasswordResetLinkMail", ex);
}
return null;
}
});
} catch (Exception ex) {
LOG.error("Unexpected error while sendPasswordResetLinkMail", ex);
}
}
/*
* Use instead of UrlParams#generateParamsUrl() cause it uses com.google.gwt.http.client.URL that stop
* execution of code on server side.
* */
private String generateParamsUrl(Map<String, String> paramsMap) {
StringBuilder paramsUrl = new StringBuilder();
for (Map.Entry<String, String> entry : paramsMap.entrySet()) {
String val = entry.getValue();
if (paramsUrl.length() > 0) {
paramsUrl.append("&");
}
paramsUrl.append(entry.getKey())
.append("=")
.append(UrlEscapers.urlPathSegmentEscaper().escape(val));
}
return paramsUrl.toString();
}
@Override
public void sendPasswordAfterReset(final String username,
final String password,
final String email) {
try {
callAsync(new Callable<Void>() {
@Override
public Void call() throws Exception {
String subject = messages.getMessage(
"passwordWasResetMailMessageSubject", new Object[]{appName}, Locale.ENGLISH);
String text = messages.getMessage(
"passwordWasResetMailMessageBody",
new Object[]{username, appBaseUrl, appName, password},
Locale.ENGLISH);
MimeMessage mimeMsg = kaaMessagingMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, "UTF-8");
try {
helper.setFrom(mailFrom);
helper.setTo(email);
helper.setSubject(subject);
helper.setText(text, true);
kaaMessagingMailSender.send(helper.getMimeMessage());
} catch (MessagingException ex) {
LOG.error("Unexpected error while sendPasswordAfterResetMail", ex);
}
return null;
}
});
} catch (Exception ex) {
LOG.error("Unexpected error while sendPasswordAfterResetMail", ex);
}
}
private <T> void callAsync(Callable<T> callable) throws IOException, InterruptedException {
sendPool.submit(callable);
}
@SuppressWarnings("unused")
private <T> T callWithTimeout(Callable<T> callable) throws IOException, InterruptedException {
Future<T> future = sendPool.submit(callable);
try {
if (sendTimeout > 0) {
return future.get(sendTimeout, TimeUnit.SECONDS);
} else {
return future.get();
}
} catch (TimeoutException ex) {
future.cancel(true);
throw new IOException("Callable timed out after " + sendTimeout
+ " sec", ex);
} catch (ExecutionException e1) {
Throwable cause = e1.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof InterruptedException) {
throw (InterruptedException) cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
} else {
throw new RuntimeException(e1);
}
} catch (CancellationException ce) {
LOG.error("Blocked callable interrupted by rotation event", ce);
throw new InterruptedException(
"Blocked callable interrupted by rotation event");
} catch (InterruptedException ex) {
LOG.warn("Unexpected Exception {}", ex.getMessage(), ex);
throw ex;
}
}
}