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.time.Instant;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;

import javax.mail.Session;
import javax.mail.internet.MimeMessage;

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.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;

	@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);
		var resource = appResourceService.getResource(AppResourceService.APP_NAME_GG_CE, "email.template", Locale.getDefault());
		if (resource != null) {
			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
			var styleStartIndex = text.indexOf("<style>");
			var styleEndIndex = text.lastIndexOf("</style>");
			if (styleStartIndex != -1 && styleEndIndex != -1) {
				var extraStyles = text.substring(styleStartIndex, styleEndIndex + "</style>".length());
				scopes.put("extraStyles", extraStyles);
				scopes.put("messageBody", text.replace(extraStyles, ""));
			}

			messageText.set(templatingService.fillTemplate(resource.getDescription(), scopes));
		} else { 
			// Handle missing template
			messageText.set("<DOCTYPE html>\n\n<html><body>" + messageText.get() + "</body></html>");
		}

		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);
	}
}