Monday, April 6, 2009

Adding TLS support to Log4j SMTP Appender

SMTPAppender that comes with log4j is a pretty useful Appender, allowing easily to start getting email alerts for errors in your application. But, this class is missing one important property (well, maybe more than one...): TLS support. Most modern mail servers use TLS for sending mails. TLS (Transport Layer Security) is a secure way for transfering information between two machines. For example, if you are using Google Apps (or even if you have a regular Gmail account) and you would like to use your account (user name and password of course) to send mail using SMTPAppender you won't be able to do it. That is because, Google allows sending mails only using TLS.

In this post: "Sending Email alerts with Log4j – Controlled Alerts" I showed how log4j SMTPAppender class can be extended to allow email alerts to be more controlled. In this post: "Sending SMS alerts with Log4j using ipipi.com", I showed how log4j SMTPAppender can be extended to send SMS error messages using ipipi.com service.
Both posts use this
class: BaseFilteredSMTPAppender as a basic class for adding more neat capabilities to SMTPAppender.

I will show you how this class (BaseFilteredSMTPAppender) can be easily changed, to add the SMTPAppender TLS capabilities.
Unfortunatly, SMTPAppender class was not designed so well. It does not allow any control over the properties used for the creation of javax.mail.Session instance. In order to add TLS support to mail sending, we simply have to add to the javax.mail.Session class the following property:

props.put("mail.smtp.starttls.enable","true");
Where props is a simple Property instance containing mail properties.
And then we get the session instance, for example, by doing:
Session session = Session.getInstance(props);
But, as was said before, we have no access to the Properties instance, and therefore cannot add the TLS property.

Luckily, SMTPAppender does contain a protected method, for getting the Session instance: createSession. We can override this method and create a Session instance that contains TLS support. I copied the code of this method exactly as it was on the original SMTPAppender, and simply added the TLS property (note that TLS support is added only if user actually use the TLS property in the appender definition). This is how the class BaseFilteredSMTPAppender looks after adding the TLS support:

import org.apache.log4j.net.SMTPAppender;
import javax.mail.Authenticator;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import java.util.*;
public abstract class BaseFilteredSMTPAppender extends SMTPAppender {
  private int timeFrame;
  private int maxEMails;
  protected long timeFrameMillis;
  protected Boolean isTLS;
  protected List<Date> exceptionDates = new ArrayList<Date>();
  public int getTimeFrame() {
    return timeFrame;
  }
  public void setTimeFrame(int timeFrame) {
    this.timeFrame = timeFrame;
  }
  public int getMaxEMails() {
    return maxEMails;
  }
  public void setMaxEMails(int maxEMails) {
    this.maxEMails = maxEMails;
  }
  public void setTLS(boolean isTLS) {
    this.isTLS = isTLS;
  }
  @Override
  public void activateOptions() {
    super.activateOptions();
    timeFrameMillis = timeFrame * 60 * 1000;
  }
  @Override
  protected Session createSession() {
    Properties props = null;
    try {
        props = new Properties (System.getProperties());
    } catch(SecurityException ex) {
        props = new Properties();
    }
    if (getSMTPHost() != null) {
      props.put("mail.smtp.host", getSMTPHost());
    }
    Authenticator auth = null;
    if(getSMTPUsername() != null && getSMTPPassword() != null) {
      props.put("mail.smtp.auth", "true");
      auth = new Authenticator() {
        protected PasswordAuthentication getPasswordAuthentication() {
          return new PasswordAuthentication(getSMTPUsername(), getSMTPPassword());
        }
      };
    }
    if (isTLS != null && isTLS)
    {
      props.put("mail.smtp.starttls.enable","true");
    }
    Session session = Session.getInstance(props, auth);
    if (getSMTPDebug()) {
        session.setDebug(getSMTPDebug());
    }
    return session;
  }
  protected void cleanTimedoutExceptions() {
    Date current = new Date();
    // Remove timedout exceptions
    Iterator<Date> itr = exceptionDates.iterator();
    while (itr.hasNext()) {
      Date exceptionDate = itr.next();
      if (current.getTime() - exceptionDate.getTime() > timeFrameMillis) {
        itr.remove();
      } else {
        break;
      }
    }
  }
  protected void addException() {
    exceptionDates.add(new Date());
  }
  protected boolean isSendMailAllowed() {
    return exceptionDates.size() < maxEMails;
  }
}

In order to use the Appender, only one additional parameter has to be added to the parameters already used and shown in the previous 2 blogs dealing with log4j SMTPAppender: TLS.

In order to make things a little interesting I will show you log4j configuration example using XML file instead of properties file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">  
   <appender name="console" class="org.apache.log4j.ConsoleAppender">
      <param name="Target" value="System.out"/>
      <layout class="org.apache.log4j.PatternLayout">
         <param name="ConversionPattern" value="%d{HH:mm:ss,SSS} %-5p [%c{1}] %m%n"/>
      </layout>
   </appender>
    <appender name="email" class="com.bashan.log4j.appender.FilteredSMTPAppender">
        <param name="BufferSize" value="10"/>
        <param name="SMTPHost" value="smtp.gmail.com"/>
        <param name="SMTPUsername" value="username@gmail.com"/>
        <param name="SMTPPassword" value="password"/>
        <param name="TLS" value="true"/>
        <param name="TimeFrame" value="10"/>
        <param name="MaxEMails" value="2"/>
        <param name="From" value="username@gmail.com"/>
        <param name="To" value="anotherUsername@gmail.com"/>
        <param name="Subject" value="Server Error"/>
        <layout class="org.apache.log4j.PatternLayout">
          <param name="ConversionPattern" value="%d{HH:mm:ss,SSS} %-5p [%c{1}] %m%n"/>
        </layout>
    </appender>  
    <root>
      <priority value="warn"/>
      <appender-ref ref="console"/>
      <appender-ref ref="email"/>
   </root>
</log4j:configuration>

This file can be dropped on your root src directory and log4j will know to find and load it automatically.

Note that TLS property is not mandatory. If you don't need TLS support, you can simply not add it to the appender properties.

7 comments:

  1. Thank you very much.. I couldn't find this help anywhere else.

    ReplyDelete
  2. Amazing! Good work.

    ReplyDelete
  3. Excellent!!!. Thank u so much.

    ReplyDelete
  4. Thank You very much - it works !

    ReplyDelete
  5. For some reason, the STARTTLS option would not work, and the above code did not work for me as-is for gmail/smtp (Where do you specify the port?). However, the approach was clear and I added the required code to make sure it works with ssl port 465 by adding the additional required parameters.

    Thanks very much for the sample...

    ReplyDelete
  6. Thanks a lot for this post, it really helped me ou

    ReplyDelete
  7. Thanks a lot for this post, it really helped me out

    ReplyDelete