EMailServiceImpl.java
/*
* Copyright 2020 Global Crop Diversity Trust
*
* 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.gringlobal.service.impl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.mail.Session;
import javax.mail.internet.MimeMessage;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.apache.commons.lang3.StringUtils;
import org.gringlobal.model.Email;
import org.gringlobal.model.QEmail;
import org.gringlobal.persistence.EmailRepository;
import org.gringlobal.service.AppResourceService;
import org.gringlobal.service.EMailService;
import org.gringlobal.service.TemplatingService;
import org.gringlobal.service.UserService;
import org.gringlobal.spring.TransactionHelper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class EMailServiceImpl implements EMailService, InitializingBean {
@Autowired(required = false)
private JavaMailSender mailSender;
@Autowired
private ThreadPoolTaskExecutor executor;
@Autowired
private TemplatingService templatingService;
@Autowired
private AppResourceService appResourceService;
@Autowired
private UserService userService;
@Autowired
private EmailRepository emailRepository;
@Autowired
private JPAQueryFactory jpaQueryFactory;
@Value("${mail.async}")
private boolean async;
@Value("${mail.user.from}")
private String emailFrom;
@Value("${base.url}")
private String baseUrl;
@Value("${frontend.url}")
private String frontendUrl;
@Value("${auditlog.retentionPeriod:3M}") // week
private String emailLogRetention;
private Period emailLogRetentionPeriod;
private final Cache<String, String> templateCache = CacheBuilder.newBuilder().maximumSize(2).expireAfterAccess(1, TimeUnit.MINUTES).build();
@Override
public void afterPropertiesSet() throws Exception {
emailLogRetention = emailLogRetention.replaceAll(" ", "");
emailLogRetentionPeriod = Period.parse("P".concat(emailLogRetention));
}
@Transactional
@Scheduled(initialDelayString = "PT2H", fixedDelayString = "PT8H")
@SchedulerLock(name = "org.gringlobal.service.impl.EmailServiceImpl")
public void cleanExpiredEmailLogs() {
Instant expiredInstant = ZonedDateTime.now().minus(emailLogRetentionPeriod).toInstant();
log.trace("Searching for the email logs created before: {}", expiredInstant);
var removed = jpaQueryFactory.delete(QEmail.email).where(QEmail.email.createdDate.before(expiredInstant)).execute();
log.warn("Removed {} email log entries older than {}. Retention period is {}.", removed, expiredInstant, emailLogRetentionPeriod);
}
@Override
public String[] toEmails(String emailAddresses) {
if (StringUtils.isBlank(emailAddresses)) {
return null;
}
String[] res = emailAddresses.split(",|;");
for (int i = 0; i < res.length; i++) {
res[i] = StringUtils.trim(res[i]);
}
return res;
}
@Override
public void sendMail(String mailSubject, String mailBody, String... emailTo) {
sendMail(mailSubject, mailBody, null, emailTo);
}
@Override
public void sendMail(String mailSubject, String mailBody, String[] emailCc, String... emailTo) {
sendSimpleEmail(mailSubject, mailBody, emailFrom, emailCc, emailTo);
}
public void sendSimpleEmail(final String subject, final String text, final String emailFrom, final String[] emailCc, final String... emailTo) {
if (mailSender == null) {
log.warn("SMTP not enabled, email was not sent.");
return;
}
AtomicReference<String> messageText = new AtomicReference<>(text);
String emailTemplate;
try {
emailTemplate = templateCache.get("email.template", () -> {
log.info("Loading email.template");
var resource = appResourceService.getResource(AppResourceService.APP_NAME_GGCE, "email/template.html", LocaleContextHolder.getLocale());
var template = "<DOCTYPE html>\n\n<html><head>{{extraStyles}}<body>{{messageBody}}</body></html>";
if (resource != null) {
template = StringUtils.defaultIfBlank(resource.getDisplayMember(), template);
} else {
try {
log.warn("AppResource 'email/template.html' not found, using bundled template");
template = Files.readString(Path.of(getClass().getResource("/email/template.html").getPath()));
} catch (IOException e) {
log.warn("Could not load email template from resources: {}", e.getMessage());
}
}
return template;
});
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
final Map<String, Object> scopes = new HashMap<>();
scopes.put("messageBody", text);
scopes.put("subject", subject);
scopes.put("frontendUrl", frontendUrl);
scopes.put("baseUrl", baseUrl);
scopes.put("serverName", "GGCE");
// Extra styles
List<String> extraStyles = new LinkedList<>();
var textWithoutStyles = Pattern.compile("<style>(.(?!<\\/style>))*.?<\\/style>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(text).replaceAll(match -> {
log.trace("Adding to extraStyes:\n{}", match.group());
if (!extraStyles.contains(match.group())) extraStyles.add(match.group()); // Add if not yet added, keep order
return "";
});
scopes.put("extraStyles", extraStyles.stream().collect(Collectors.joining("\n")));
scopes.put("messageBody", textWithoutStyles);
messageText.set(templatingService.fillTemplate(emailTemplate, scopes));
final MimeMessagePreparator preparator = mimeMessage -> {
final MimeMessageHelper message = new MimeMessageHelper(mimeMessage, "UTF-8");
message.setFrom(emailFrom);
message.setTo(emailTo);
if (emailCc != null && emailCc.length > 0) {
message.setCc(emailCc);
}
message.setSubject(subject);
message.setText(messageText.get(), true);
};
if (log.isInfoEnabled()) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
var message = new MimeMessage((Session)null);
preparator.prepare(message);
message.writeTo(baos);
log.info("Outgoing message:\n{}\n<EOF>", baos);
} catch (Exception e) {
log.info(e.getMessage(), e);
}
}
JavaMailSenderImpl javaMailSender = (JavaMailSenderImpl) mailSender;
if (async) {
// execute sender in separate thread
log.info("Sending email message asynchroniously");
executor.submit(() -> {
try {
log.warn("Sending email now via {}:{}", javaMailSender.getHost(), javaMailSender.getPort());
mailSender.send(preparator);
log.warn("Email delivered to {}:{}", javaMailSender.getHost(), javaMailSender.getPort());
saveEmailLog(subject, messageText.get(), emailFrom, emailCc, emailTo);
} catch (final Throwable e) {
log.error(e.getMessage(), e);
}
});
} else {
log.warn("Sending email now via {}:{}", javaMailSender.getHost(), javaMailSender.getPort());
mailSender.send(preparator);
saveEmailLog(subject, messageText.get(), emailFrom, emailCc, emailTo);
}
}
private void saveEmailLog(final String subject, final String text, final String emailFrom, final String[] emailCc, final String[] emailTo) {
try {
asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> {
Email email = new Email();
email.setSubject(subject);
email.setBody(text);
email.setEmailFrom(emailFrom);
email.setEmailCc(emailCc == null ? null : String.join(";", emailCc));
email.setEmailTo(String.join(";", emailTo));
email.setSentDate(new Date());
email.setToBeSentDate(new Date());
email.setIsHtml("Y");
emailRepository.save(email);
return true;
}));
} catch (Exception e) {
log.error("Unable to log email", e);
}
}
private <T> void asAdmin(Callable<T> callable) throws Exception {
UserDetails administrator = userService.loadUserByUsername("administrator");
List<GrantedAuthority> authorities = Lists.newArrayList(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
authorities.addAll(administrator.getAuthorities());
Authentication authentication = new UsernamePasswordAuthenticationToken(administrator, null, authorities);
TransactionHelper.asUser(authentication, callable);
}
}