package com.intrbiz.bergamot.notification.engine.email;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
import javax.mail.Message;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.apache.log4j.Logger;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Timer;
import com.intrbiz.Util;
import com.intrbiz.accounting.Accounting;
import com.intrbiz.bergamot.accounting.model.SendNotificationToContactAccountingEvent;
import com.intrbiz.bergamot.health.HealthTracker;
import com.intrbiz.bergamot.health.model.KnownDaemon;
import com.intrbiz.bergamot.model.message.ContactMO;
import com.intrbiz.bergamot.model.message.notification.CheckNotification;
import com.intrbiz.bergamot.model.message.notification.GenericNotification;
import com.intrbiz.bergamot.model.message.notification.Notification;
import com.intrbiz.bergamot.model.message.notification.SendRecovery;
import com.intrbiz.bergamot.notification.AbstractNotificationEngine;
import com.intrbiz.configuration.CfgParameter;
import com.intrbiz.gerald.source.IntelligenceSource;
import com.intrbiz.gerald.witchcraft.Witchcraft;
import com.intrbiz.queue.QueueException;
public class EmailEngine extends AbstractNotificationEngine
{
public static final String NAME = "email";
private Logger logger = Logger.getLogger(EmailEngine.class);
private Properties properties = new Properties();
private Session session;
private String user;
private String password;
private String fromAddress;
private final Timer emailSendTimer;
private final Counter emailSendErrors;
private Accounting accounting = Accounting.create(AbstractNotificationEngine.class);
private List<String> healthcheckAdmins = new LinkedList<String>();
public EmailEngine()
{
super(NAME);
// setup metrics
IntelligenceSource source = Witchcraft.get().source("com.intrbiz.bergamot.email");
this.emailSendTimer = source.getRegistry().timer("email-sent");
this.emailSendErrors = source.getRegistry().counter("email-errors");
}
public List<String> getHealthcheckAdmins()
{
return healthcheckAdmins;
}
@Override
protected void configure() throws Exception
{
super.configure();
// setup the JavaMail properties
this.properties = new Properties();
this.properties.put("mail.smtp.host", this.config.getStringParameterValue("mail.host", "127.0.0.1"));
this.properties.put("mail.smtp.port", this.config.getStringParameterValue("mail.port", "25"));
if (this.config.getBooleanParameterValue("mail.tls", false))
{
this.properties.put("mail.smtp.port", this.config.getStringParameterValue("mail.port", "587"));
this.properties.put("mail.smtp.starttls.enable", true);
this.properties.put("mail.smtp.auth", true);
}
this.properties.put("mail.smtp.from", this.config.getStringParameterValue("from", "bergamot@localhost"));
// auth details
this.user = this.config.getStringParameterValue("mail.user", "");
this.password = this.config.getStringParameterValue("mail.password", "");
// from address
this.fromAddress = this.config.getStringParameterValue("from", "bergamot@localhost");
// setup the JavaMail session
this.session = Session.getDefaultInstance(properties);
this.session.setDebug(true);
// who to contact in the event we get a warning from the healthcheck subsystem
for (CfgParameter param : this.config.getParameters())
{
if ("healthcheck.admin".equals(param.getName()) && (! Util.isEmpty(param.getValueOrText())))
this.healthcheckAdmins.add(param.getValueOrText());
}
logger.info("Healthcheck alerts will be sent to " + this.healthcheckAdmins);
// setup healthchecking
HealthTracker.getInstance().addAlertHandler(this::raiseHealthcheckAlert);
}
public void raiseHealthcheckAlert(KnownDaemon failed)
{
logger.error("Got healthcheck alert for " + failed.getDaemonName() + " [" + failed.getInstanceId() + "] on host " + failed.getHostName() + " [" + failed.getHostId() + "]");
if (! this.healthcheckAdmins.isEmpty())
{
// really try to send
for (int i = 0; i < 10; i++)
{
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
try
{
// build the message
Message message = this.buildHealhCheckMessage(this.session, failed);
// send the message
Transport transport = null;
try
{
transport = session.getTransport("smtp");
// connect
if (Util.isEmpty(this.user))
{
logger.debug("Transport connecting to: " + this.properties.getProperty("mail.smtp.host"));
transport.connect();
logger.debug("Transport connected");
}
else
{
logger.debug("Transport connecting to: " + this.properties.getProperty("mail.smtp.host") + " with username: " + this.user);
transport.connect(this.user, this.password);
logger.debug("Transport connected");
}
// send the message
logger.info("Sending healthcheck message...");
transport.sendMessage(message, message.getAllRecipients());
logger.info("Message healthcheck sent");
}
finally
{
if (transport != null) transport.close();
}
// successfully sent
break;
}
catch (Throwable e)
{
logger.error("Failed to send healthcheck email notification", e);
}
// pause after each attempt
if (i > 0)
{
try
{
Thread.sleep(i * 1000);
}
catch (InterruptedException e)
{
}
}
}
}
else
{
logger.info("No admins configured, not sending health check alert");
}
}
protected Message buildHealhCheckMessage(Session session, KnownDaemon daemon) throws Exception
{
Message message = new MimeMessage(session);
// from
message.setFrom(new InternetAddress(this.fromAddress));
// to address
for (String admin : this.healthcheckAdmins)
{
message.addRecipient(RecipientType.TO, new InternetAddress(admin));
}
// sent date
message.setSentDate(new Date());
// content
message.setSubject(this.applyTemplate("healthcheck.alert.subject", daemon));
message.setContent(this.applyTemplate("healthcheck.alert.content", daemon), "text/plain");
return message;
}
@Override
public void sendNotification(Notification notification)
{
if (logger.isTraceEnabled()) logger.trace(notification.toString());
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
logger.info("Sending email notification for " + notification.getNotificationType() + " to " + notification.getTo().stream().map(ContactMO::getEmail).filter((e) -> { return e != null; }).collect(Collectors.toList()));
Timer.Context tctx = this.emailSendTimer.time();
try
{
if (! this.checkAtLeastOneRecipient(notification))
{
logger.info("Not sending email, no recipients");
return;
}
// build the message
Message message = this.buildMessage(notification);
logger.debug("Built message");
// send the message
Transport transport = null;
try
{
transport = session.getTransport("smtp");
// connect
if (Util.isEmpty(this.user))
{
logger.debug("Transport connecting to: " + this.properties.getProperty("mail.smtp.host"));
transport.connect();
logger.debug("Transport connected");
}
else
{
logger.debug("Transport connecting to: " + this.properties.getProperty("mail.smtp.host") + " with username: " + this.user);
transport.connect(this.user, this.password);
logger.debug("Transport connected");
}
// send the message
logger.info("Sending message...");
transport.sendMessage(message, message.getAllRecipients());
logger.info("Message sent");
// accounting
for (ContactMO contact : notification.getTo())
{
if ((!Util.isEmpty(contact.getEmail())) && contact.hasEngine(this.getName()))
{
this.accounting.account(new SendNotificationToContactAccountingEvent(
notification.getSite().getId(),
notification.getId(),
getObjectId(notification),
getNotificationType(notification),
contact.getId(),
this.getName(),
"email",
contact.getEmail(),
Util.nullable(message.getHeader("Message-ID"), (s) -> s.length == 0 ? null : s[0])
));
}
}
}
finally
{
if (transport != null) transport.close();
}
}
catch (Throwable e)
{
this.emailSendErrors.inc();
logger.error("Failed to send email notification", e);
throw new QueueException("Failed to send email notification", e);
}
finally
{
tctx.stop();
}
}
protected boolean checkAtLeastOneRecipient(Notification notification)
{
for (ContactMO contact : notification.getTo())
{
if ((!Util.isEmpty(contact.getEmail())) && contact.hasEngine(this.getName()))
{
return true;
}
}
return false;
}
protected Message buildMessage(Notification notification) throws Exception
{
if (notification instanceof CheckNotification)
{
return this.buildCheckMessage((CheckNotification) notification);
}
else if (notification instanceof GenericNotification)
{
return this.buildGenericMessage((GenericNotification) notification);
}
return null;
}
protected Message buildGenericMessage(GenericNotification notification) throws Exception
{
Message message = new MimeMessage(this.session);
// from
message.setFrom(new InternetAddress(this.fromAddress));
// to address
for (ContactMO contact : notification.getTo())
{
if ((!Util.isEmpty(contact.getEmail())) && contact.hasEngine(this.getName()))
{
message.addRecipient(RecipientType.TO, new InternetAddress(contact.getEmail()));
}
}
// headers
message.setHeader("X-Bergamot-Notification-Id", notification.getId().toString());
// sent date
message.setSentDate(new Date(notification.getRaised()));
// content
this.buildGenericMessageContent(message, notification);
return message;
}
protected Message buildCheckMessage(CheckNotification notification) throws Exception
{
Message message = new MimeMessage(this.session) {
@Override
protected void updateMessageID() throws MessagingException
{
// do not alter the message id
}
};
// from
message.setFrom(new InternetAddress(this.fromAddress));
// to address
for (ContactMO contact : notification.getTo())
{
if ((!Util.isEmpty(contact.getEmail())) && contact.hasEngine(this.getName()))
{
message.addRecipient(RecipientType.TO, new InternetAddress(contact.getEmail()));
}
}
// headers
message.setHeader("X-Bergamot-Alert-Id", notification.getAlertId().toString());
message.setHeader("Message-ID", this.checkMessageId(notification));
message.setHeader("X-Bergamot-Notification-Id", notification.getId().toString());
message.setHeader("X-Bergamot-Check-Type", notification.getCheck().getType());
message.setHeader("X-Bergamot-Check-Id", notification.getCheck().getId().toString());
message.setHeader("X-Bergamot-Check-Status", notification.getCheck().getState().getStatus().toString());
// in reply to?
if (notification instanceof SendRecovery)
{
message.setHeader("References", this.checkMessageId(notification));
message.setHeader("In-Reply-To", this.checkMessageId(notification));
}
// sent date
message.setSentDate(new Date(notification.getRaised()));
// content
this.buildCheckMessageContent(message, notification);
return message;
}
protected String checkMessageId(CheckNotification notification)
{
return "<" + notification.getAlertId().toString() + ".bergamot>";
}
protected void buildCheckMessageContent(Message message, CheckNotification notification) throws Exception
{
String templatePrefix = notification.getCheck().getCheckType() + "." + notification.getNotificationType() + ".";
message.setSubject(this.applyTemplate(templatePrefix + "subject", notification));
message.setContent(this.applyTemplate(templatePrefix + "content", notification), "text/plain");
}
protected void buildGenericMessageContent(Message message, GenericNotification notification) throws Exception
{
String templatePrefix = notification.getNotificationType() + ".";
message.setSubject(this.applyTemplate(templatePrefix + "subject", notification));
message.setContent(this.applyTemplate(templatePrefix + "content", notification), "text/plain");
}
}