package rocks.inspectit.server.mail;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.mail.AuthenticationFailedException;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Transport;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.mail.DefaultAuthenticator;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import rocks.inspectit.server.externalservice.IExternalService;
import rocks.inspectit.shared.all.cmr.property.spring.PropertyUpdate;
import rocks.inspectit.shared.all.externalservice.ExternalServiceStatus;
import rocks.inspectit.shared.all.externalservice.ExternalServiceType;
import rocks.inspectit.shared.all.spring.logger.Log;
import rocks.inspectit.shared.all.util.EMailUtils;
/**
* Central component for sending e-mails.
*
* @author Alexander Wert
* @author Marius Oehler
*
*/
@Component
public class EMailSender implements IExternalService {
/**
* The delay in seconds between consecutive automatic connection checks.
*/
private static final Long[] EXECUTION_DELAYS = { 15L, 30L, 60L, 120L };
/**
* Logger for the class.
*/
@Log
Logger log;
/**
* SMTP Server enabled.
*/
@Value("${mail.enable}")
boolean smtpEnabled;
/**
* SMTP Server host.
*/
@Value("${mail.smtp.host}")
String smtpHost;
/**
* SMTP Server port.
*/
@Value("${mail.smtp.port}")
int smtpPort;
/**
* SMTP user name.
*/
@Value("${mail.smtp.user}")
String smtpUser;
/**
* Password for SMTP authentication.
*/
@Value("${mail.smtp.passwd}")
String smtpPassword;
/**
* The e-mail address used as sender.
*/
@Value("${mail.from}")
String senderAddress;
/**
* Displayed name of the sender.
*/
@Value("${mail.from.name}")
String senderName;
/**
* A comma separated list of default recipient e-mail addresses.
*/
@Value("${mail.default.to}")
String defaultRecipientString;
/**
* Additional SMTP properties as a comma separated string.
*/
@Value("${mail.smtp.properties}")
String smtpPropertiesString;
/**
* Whether a test mail should be send after SMTP property update.
*/
@Value("${mail.test.enabled}")
boolean testMailEnabled;
/**
* The recipient address for the test e-mail.
*/
@Value("${mail.test.recipient}")
String testMailRecipient;
/**
* Unwrapped list of default recipients.
*/
private List<String> defaultRecipients = new ArrayList<>();
/**
* SMTP connection state.
*/
private boolean connected = false;
/**
* Additional SMTP properties that might be required for certain SMTP servers.
*/
private Properties additionalProperties = new Properties();
/**
* The {@link ObjectFactory} used to created object instances.
*/
private ObjectFactory objectFactory = new ObjectFactory();
/**
* {@link ExecutorService} instance.
*/
@Autowired
@Resource(name = "scheduledExecutorService")
private ScheduledExecutorService scheduledExecutorService;
/**
* Instance of {@link ConnectionCheck}.
*/
private final ConnectionCheck connectionCheck = new ConnectionCheck();
/**
* The future of the executed {@link ConnectionCheck}.
*/
private Future<?> connectionCheckFuture;
/**
* The index of the next used execution delay.
*/
private int executionDelayIndex = 0;
/**
* {@inheritDoc}
*/
public boolean sendEMail(String subject, String htmlMessage, String textMessage, List<String> recipients) {
if (StringUtils.isEmpty(subject)) {
throw new IllegalArgumentException("The given subject may not be null or empty.");
}
if (StringUtils.isEmpty(htmlMessage)) {
throw new IllegalArgumentException("The given HTML body may not be null or empty.");
}
if (StringUtils.isEmpty(textMessage)) {
throw new IllegalArgumentException("The given text body may not be null or empty.");
}
if (!connected) {
log.warn("Failed sending e-mail! E-Mail service cannot connect to the SMTP server. Check the connection settings!");
return false;
}
try {
HtmlEmail email = prepareHtmlEmail(recipients);
email.setSubject(subject);
email.setHtmlMsg(htmlMessage);
email.setTextMsg(textMessage);
email.send();
return true;
} catch (EmailException | IllegalArgumentException e) {
if (log.isWarnEnabled()) {
log.warn("Failed sending e-mail!", e);
}
return false;
}
}
/**
* {@inheritDoc}
*/
public boolean isConnected() {
return getServiceStatus() == ExternalServiceStatus.CONNECTED;
}
/**
* Unwrap the comma separated list string of default recipients into a real list.
*/
@PropertyUpdate(properties = { "mail.default.to" })
private void parseRecipientsString() {
defaultRecipients.clear();
if (null != defaultRecipientString) {
String[] strArray = defaultRecipientString.split(",");
for (String element : strArray) {
String address = element.trim();
if (EMailUtils.isValidEmailAddress(address)) {
defaultRecipients.add(address);
}
}
}
}
/**
* Unwrap the comma separated list string of additional properties into real properties object.
*/
private void parseAdditionalPropertiesString() {
additionalProperties.clear();
if (null != smtpPropertiesString) {
String[] strArray = smtpPropertiesString.split(",");
for (String property : strArray) {
int equalsIndex = property.indexOf('=');
if ((equalsIndex > 0) && (equalsIndex < (property.length() - 1))) {
additionalProperties.put(property.substring(0, equalsIndex).trim(), property.substring(equalsIndex + 1).trim());
}
}
}
}
/**
* Parses the SMTP properties and checks whether a connection can be established.
*/
@PropertyUpdate(properties = { "mail.smtp.host", "mail.smtp.port", "mail.smtp.user", "mail.smtp.passwd", "mail.smtp.properties", "mail.test.enabled" })
public void onSmtpPropertiesChanged() {
if (!smtpEnabled) {
return;
}
onSmtpPropertiesChanged(true);
}
/**
* Parses the SMTP properties and checks whether a connection can be established.
*
* @param sendTestMailIfEnabled
* send a test email if this feature is enabled
*/
private void onSmtpPropertiesChanged(boolean sendTestMailIfEnabled) {
parseAdditionalPropertiesString();
checkConnection(sendTestMailIfEnabled && testMailEnabled);
}
/**
* Initialize E-Mail service.
*/
@PropertyUpdate(properties = { "mail.enable" })
public void init() {
init(true);
}
/**
* Method which is executed after bean is created.
*/
@PostConstruct
public void postConstruct() {
init(false);
}
/**
* Initialize E-Mail service.
*
* @param sendTestMailIfEnabled
* send a test email if this feature is enabled
*/
private void init(boolean sendTestMailIfEnabled) {
if (!smtpEnabled) {
return;
} else if (log.isInfoEnabled()) {
log.info("|-eMail Service initialized");
}
parseRecipientsString();
onSmtpPropertiesChanged(sendTestMailIfEnabled);
}
/**
* Checks connection to SMTP server.
*
* @param sendTestMail
* specifies whether a test email should be send after the connection is established
*/
private void checkConnection(boolean sendTestMail) {
if ((connectionCheckFuture != null) && !connectionCheckFuture.isDone()) {
connectionCheckFuture.cancel(true);
}
connectionCheck.sendTestEmail = sendTestMail;
connectionCheckFuture = scheduledExecutorService.submit(connectionCheck);
}
/**
* Prepares an email object including the default recipients.
*
* @param recipients
* recipient to send to.
* @return Returns a prepared {@link HtmlEmail} object.
* @throws EmailException
* is thrown when the from address could not be set
*/
private HtmlEmail prepareHtmlEmail(List<String> recipients) throws EmailException {
return prepareHtmlEmail(recipients, true);
}
/**
* Prepares an email object.
*
* @param recipients
* recipient to send to.
* @param includeDefaultRecipients
* whether the default recipients should be added to the mail
* @return Returns a prepared {@link HtmlEmail} object.
* @throws EmailException
* is thrown when the from address could not be set
*/
private HtmlEmail prepareHtmlEmail(List<String> recipients, boolean includeDefaultRecipients) throws EmailException {
HtmlEmail email = objectFactory.createHtmlEmail();
email.setHostName(smtpHost);
email.setSmtpPort(smtpPort);
email.setAuthentication(smtpUser, smtpPassword);
email.setFrom(senderAddress, senderName);
if ((additionalProperties != null) && !additionalProperties.isEmpty()) {
email.getMailSession().getProperties().putAll(additionalProperties);
}
if (includeDefaultRecipients) {
for (String defaultTo : defaultRecipients) {
try {
email.addTo(defaultTo);
} catch (EmailException e) {
if (log.isWarnEnabled()) {
log.warn("Invalid recipient e-mail address!", e);
}
}
}
}
if (recipients != null) {
for (String to : recipients) {
try {
email.addTo(to);
} catch (EmailException e) {
if (log.isWarnEnabled()) {
log.warn("Invalid recipient e-mail address!", e);
}
}
}
}
return email;
}
/**
* * Sending a test e-mail to the configured recipient if a valid SMTP server has been
* configured and connected.
*/
@PropertyUpdate(properties = { "mail.test.recipient" })
private void sendTestMail() {
if (getServiceStatus() == ExternalServiceStatus.DISABLED) {
return;
} else if (!isConnected()) {
if (log.isInfoEnabled()) {
log.info("Cannot send a test e-mail because the SMTP connection is not established.");
}
return;
} else if (StringUtils.isEmpty(testMailRecipient)) {
if (log.isInfoEnabled()) {
log.info("A recipient address has to be specified in order to send a test e-mail.");
}
return;
} else if (!EMailUtils.isValidEmailAddress(testMailRecipient)) {
if (log.isWarnEnabled()) {
log.warn("The specified recipient address for the test e-mail is not valid.");
}
return;
}
if (log.isDebugEnabled()) {
log.debug("Sending test e-mail to '{}'.", testMailRecipient);
}
try {
HtmlEmail htmlEmail = prepareHtmlEmail(Collections.singletonList(testMailRecipient), false);
htmlEmail.setSubject("inspectIT Test E-Mail");
htmlEmail.setTextMsg("Hello, this is a test e-mail. You have successfully configured the SMTP server used by inspectIT!");
htmlEmail.send();
if (log.isInfoEnabled()) {
log.info("Successfully sent test e-mail to '{}'.", testMailRecipient);
}
} catch (EmailException | IllegalArgumentException e) {
if (log.isWarnEnabled()) {
log.warn("Failed sending test e-mail!", e);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public ExternalServiceType getServiceType() {
return ExternalServiceType.MAIL_SENDER;
}
/**
* {@inheritDoc}
*/
@Override
public ExternalServiceStatus getServiceStatus() {
if (!smtpEnabled) {
return ExternalServiceStatus.DISABLED;
}
if (connected) {
return ExternalServiceStatus.CONNECTED;
} else {
return ExternalServiceStatus.DISCONNECTED;
}
}
/**
* Factory class to create objects required by the EMailSender. This class primary exists for
* better testing process.
*
* @author Marius Oehler
*
*/
class ObjectFactory {
/**
* Get a {@link Transport} object for a SMTP connection.
*
* @return A new {@link Transport}.
* @throws NoSuchProviderException
* If provider for SMTP protocol is not found.
*/
public Transport getSmtpTransport() throws NoSuchProviderException {
return Session.getInstance(additionalProperties, new DefaultAuthenticator(smtpUser, smtpPassword)).getTransport("smtp");
}
/**
* Creates a new instance of {@link HtmlEmail}.
*
* @return the created instance
*/
public HtmlEmail createHtmlEmail() {
return new HtmlEmail();
}
}
/**
* Runnable to execute the connection check.
*
* @author Marius Oehler
*
*/
private class ConnectionCheck implements Runnable {
/**
* Specifies whether a test email should be send if the connection check was successful.
*/
private boolean sendTestEmail = false;
/**
* {@inheritDoc}
*/
@Override
public void run() {
if (log.isDebugEnabled()) {
log.debug("Check connection to SMTP server..");
}
try {
Transport transport = objectFactory.getSmtpTransport();
transport.connect(smtpHost, smtpPort, smtpUser, smtpPassword);
transport.close();
if (!connected) {
if (log.isInfoEnabled()) {
log.info("|-eMail Service connected.");
}
}
connected = true;
} catch (AuthenticationFailedException e) {
if (connected) {
if (log.isInfoEnabled()) {
log.info("|-eMail Service was not able to connect! Authentication failed! Reason: {}", e.getMessage());
}
}
connected = false;
} catch (MessagingException e) {
if (connected) {
if (log.isInfoEnabled()) {
log.info("|-eMail Service was not able to connect! Reason: {}", e.getMessage());
}
}
connected = false;
} catch (RuntimeException e) {
// this catch ensures that this runnable is not crashing
if (log.isWarnEnabled()) {
log.warn("An unexpected exception has been thrown during availability check.", e);
}
}
if (smtpEnabled) {
if (connected) {
executionDelayIndex = 0;
}
scheduledExecutorService.schedule(this, EXECUTION_DELAYS[executionDelayIndex], TimeUnit.SECONDS);
if (!connected) {
if (executionDelayIndex < (EXECUTION_DELAYS.length - 1)) {
executionDelayIndex++;
}
} else if (sendTestEmail) {
sendTestEmail = false;
sendTestMail();
}
}
}
}
}