Sunday, December 23, 2012

A Bit of Regex

Every time I want to use a regular expression in a Java program, I always seem to go through a lot of iterations developing an expression to get the specific results I need. As a result, this is a small Java program that allows me to use command line arguments to specify text and expressions on the command line.

import java.util.*;
import java.util.regex.*;

public class Regex
{
    public static void main (String [] args) throws Exception
    {
        String source = args[0];
        System.out.println("Source string: " + source);
        Pattern p = Pattern.compile(args[1]);
        System.out.println("Pattern: " + args[1]);
        Matcher m = p.matcher(source);
        System.out.println("match? " + m.matches());
        for (int i=1; i <= m.groupCount(); ++i)
        {
            System.out.println("\tGroup " + i + ":\t" + m.group(i));
        }
    }
}

Tuesday, July 31, 2012

Providing Map Context In Log4J Output

Although Log4J's PatternLayout provides a way to output specifically named values from the mapped diagnostic context (MDC), there is no mechanism in any of Log4J's layouts to simply display all of the values from the MDC. In fact, the LoggingEvent does not allow access to the MDC's map or even a copy of it.

This shortcoming was particularly annoying because at various points in my code I would want to add and remove arbitrary key/value pairs from the MDC at certain points specifically to make the information available in log files. This was why I decided to extend LoggingEvent to provide access to the set of keys in the MDC's map, thus allowing other classes aware of this functionality to access the full contents of the MDC and even to display it in layouts.

This implementation is in multiple classes provided below:
  1. OpenLoggingEvent which adds getMDCKeys() to the LoggingEvent.
  2. OpenLogger which extends Logger and changes the forcedLog() method to create an OpenLoggingEvent instead of a LoggingEvent.
  3. OpenLoggerFactory which is a LoggerFactory that will attempt to install itself as the default provider via LogManager.
  4. OpenLoggerConfigurator which can be specified by the log4j.configuratorClass system property in order for OpenLoggers to be used by default during Log4J during configuration.
  5. MDCAwareHTMLLayout which extends HTMLLayout with behavior to display the MDC map's keys and values in a HTML table.
  6. MDCAwareXMLLayout which extends XMLLayout with behavior to provide the MDC map's contents via <log4j:mdc> and <log4j:mdcEntry> tags.

 package log4j.util;

import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;

import org.apache.log4j.*;
import org.apache.log4j.spi.*;

public class OpenLoggingEvent extends LoggingEvent
{
    private transient Category logger;
    private String [] mdcKeys;

    public OpenLoggingEvent(String fqnOfCategoryClass, Category logger,
                            Priority level, Object message, Throwable throwable)
    {
        super(fqnOfCategoryClass, logger, level, message, throwable);
        this.logger = logger;
    }

    public OpenLoggingEvent(String fqnOfCategoryClass, Category logger,
                            long timeStamp, Priority level, Object message,
                            Throwable throwable)
    {
        super(fqnOfCategoryClass, logger, timeStamp, level, message, throwable);
        this.logger = logger;
    }


    // Should this be synchronized?  The superclass doesn't do it.
    public void getMDCCopy()
    {
        if (mdcKeys == null)
        {
            Hashtable mdc = MDC.getContext();
            if (mdc == null)
            {
                mdcKeys = new String [0];
            }
            else
            {
                mdcKeys = (String [])mdc.keySet().toArray(new String[mdc.size()]);
            }
        }
        super.getMDCCopy();
    }

    public Collection getMDCKeys ()
    {
        if (mdcKeys == null)
        {
            getMDCCopy();
        }
        return Arrays.asList(mdcKeys);
    }

    protected Category getLogger ()
    {
        return logger;
    }
}




package log4j.util;

import org.apache.log4j.Logger;
import org.apache.log4j.Priority;

class OpenLogger extends Logger
{
    protected OpenLogger (String name)
    {
        super(name);
    }

    protected void forcedLog (String fqcn, Priority level, Object message, Throwable t)
    {
        callAppenders(new OpenLoggingEvent(fqcn, this, level, message, t));
    }
}




package log4j.util;

import org.apache.log4j.*;
import org.apache.log4j.spi.*;

public class OpenLoggerFactory implements LoggerFactory
{
    public static void install ()
    {
        try
        {
            LogManager.setRepositorySelector(new RepositorySelector()
                {
                    private Hierarchy repository = null;

                    {
                        OpenLoggerFactory factory = new OpenLoggerFactory();
                        Logger root = factory.makeNewLoggerInstance("root");
                        root.setLevel(LogManager.getLoggerRepository().getRootLogger().getLevel());
                        repository = new FactoryConfigurableHierarchy (factory, root);
                    }

                    public LoggerRepository getLoggerRepository ()
                    {
                        return repository;
                    }
                },
                                             null);  // no guard
        }
        catch (IllegalArgumentException ohWell)
        {
            // Guard prevented new repository
            Logger.getLogger(OpenLoggerFactory.class).info("Could not set RepositorySelector", ohWell);
        }
    }

    public Logger makeNewLoggerInstance (String name)
    {
        return new OpenLogger(name);
    }
}




package log4j.util;

import java.net.URL;

import org.apache.log4j.LogManager;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.Configurator;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.xml.DOMConfigurator;

/**
   Define system property log4j.configuratorClass to log4j.util.OpenLoggerConfigurator
   in order for the Log4J startup process to use OpenLoggers during configuration.
 */

public class OpenLoggerConfigurator implements Configurator
{
    static
    {
        OpenLoggerFactory.install();
    }

    public OpenLoggerConfigurator () {}

    public void doConfigure (URL url, LoggerRepository repository)
    {
        OptionConverter.selectAndConfigure(url, null, LogManager.getLoggerRepository());
    }
}



package log4j.util;

import java.util.*;

import org.apache.log4j.*;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.spi.*;
import org.apache.log4j.helpers.Transform;

public class MDCAwareHTMLLayout extends HTMLLayout
{
    public static final Logger LOGGER = Logger.getLogger(MDCAwareHTMLLayout.class);

    private RendererSupport rendererSupport;

    public MDCAwareHTMLLayout ()
    {
        super();
        if (LOGGER.getLoggerRepository() instanceof RendererSupport)
        {
            rendererSupport = (RendererSupport)LOGGER.getLoggerRepository();
        }
    }

    public String getRendererSupportClass ()
    {
        return (rendererSupport == null)
            ? null
            : rendererSupport.getClass().getName();
    }

    public void setRendererSupportClass (String className)
    {
        try
        {
            Class c = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
            if (RendererSupport.class.isAssignableFrom(c))
            {
                rendererSupport = (RendererSupport)c.newInstance();
            }
            else
            {
                LOGGER.warn(className + " does not implement " + RendererSupport.class.getName());
            }
        }
        catch (ClassNotFoundException cnfe)
        {
            LOGGER.warn("Could not find class " + className, cnfe);
        }
        catch (IllegalAccessException illEx)
        {
            LOGGER.warn("Could not create instance of class " + className, illEx);
        }
        catch (InstantiationException isAbstract)
        {
            LOGGER.warn("Can not create instance of abstract class " + className, isAbstract);
        }
        catch (ExceptionInInitializerError uhOh)
        {
            LOGGER.warn("Could not initialize class " + className, uhOh);
        }
        catch (SecurityException secEx)
        {
            LOGGER.warn("Not allowed to create instance of class " + className, secEx);
        }
    }

    public String format (LoggingEvent event)
    {
        String formatted = super.format(event);
        if (event instanceof OpenLoggingEvent)
        {
            OpenLoggingEvent openEvent =(OpenLoggingEvent)event;
            Collection mdcKeys = new TreeSet(openEvent.getMDCKeys());
            if (mdcKeys.size() > 0)
            {
                StringBuffer buf = new StringBuffer(formatted);

                buf.append("<tr><td bgcolor=\"#EEEEEE\" class=\"MDC\" colspan=\"6\" title=\"Mapped Diagnostic Context\">")
                    .append(Layout.LINE_SEP)
                    .append("<strong>Mapped Diagnostic Context</strong>")
                    .append(Layout.LINE_SEP)
                    .append("<table border=\"1\">")
                    .append("<tr><th>Key</th>")
                    .append(Layout.LINE_SEP)
                    .append("<th>Value Class</th>")
                    .append(Layout.LINE_SEP)
                    .append("<th>Rendered Value</th></tr>")
                    .append(Layout.LINE_SEP);

                RendererMap rendererMap = null;

                if (openEvent.getLogger() != null)
                {
                    LoggerRepository maybeRendererSupport = openEvent.getLogger().getLoggerRepository();
                    if (maybeRendererSupport instanceof RendererSupport)
                    {
                        rendererMap = ((RendererSupport)maybeRendererSupport).getRendererMap();
                    }
                }
                if (rendererMap == null && rendererSupport != null)
                {
                    rendererMap = rendererSupport.getRendererMap();
                }

                for (Iterator keys = mdcKeys.iterator(); keys.hasNext();)
                {
                    String nextKey = (String)keys.next();
                    Object nextValue = event.getMDC(nextKey);
                    String val = (rendererMap == null)
                        ? String.valueOf(nextValue)
                        : rendererMap.findAndRender(nextValue);
                    buf.append("<tr><td>")
                        .append(Transform.escapeTags(nextKey))
                        .append("</td>")
                        .append(Layout.LINE_SEP)
                        .append("<td>")
                        .append(nextValue.getClass().getName())
                        .append("</td>")
                        .append(Layout.LINE_SEP)
                        .append("<td>")
                        .append(Transform.escapeTags(val))
                        .append("</td></tr>")
                        .append(Layout.LINE_SEP);
                }

                buf.append("</table></td></tr>").append(Layout.LINE_SEP);
                formatted = buf.toString();
            }
        }
        return formatted;
    }
}





package log4j.util;

import java.util.*;

import org.apache.log4j.Logger;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.spi.*;
import org.apache.log4j.xml.XMLLayout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.Transform;

public class MDCAwareXMLLayout extends XMLLayout
{
    public static final Logger LOGGER = Logger.getLogger(MDCAwareXMLLayout.class);

    private RendererSupport rendererSupport;

    public MDCAwareXMLLayout ()
    {
        super();
    }

    public String getRendererSupportClass ()
    {
        return (rendererSupport == null)
            ? null
            : rendererSupport.getClass().getName();
    }

    public void setRendererSupportClass (String className)
    {
        try
        {
            Class c = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
            if (RendererSupport.class.isAssignableFrom(c))
            {
                rendererSupport = (RendererSupport)c.newInstance();
            }
            else
            {
                LogLog.warn(className + " does not implement " + RendererSupport.class.getName());
            }
        }
        catch (ClassNotFoundException cnfe)
        {
            LogLog.warn("Could not find class " + className, cnfe);
        }
        catch (IllegalAccessException illEx)
        {
            LogLog.warn("Could not create instance of class " + className, illEx);
        }
        catch (InstantiationException isAbstract)
        {
            LogLog.warn("Can not create instance of class " + className, isAbstract);
        }
        catch (ExceptionInInitializerError uhOh)
        {
            LogLog.warn("Could not initialize class " + className, uhOh);
        }
        catch (SecurityException secEx)
        {
            LogLog.warn("Not allowed to create instance of class " + className, secEx);
        }
    }

    public String format (LoggingEvent event)
    {
        String formatted = super.format(event);

        if (event instanceof OpenLoggingEvent)
        {
            OpenLoggingEvent openEvent =(OpenLoggingEvent)event;
            Collection mdcKeys = new TreeSet(openEvent.getMDCKeys());
            if (mdcKeys.size() > 0)
            {
                StringBuffer buf = new StringBuffer(formatted);
                buf.setLength(buf.indexOf("</log4j:event") + 1);
                buf.append("<log4j:mdc>\r\n");

                RendererMap rendererMap = null;
                if (openEvent.getLogger() != null)
                {
                    LoggerRepository maybeRendererSupport = openEvent.getLogger().getLoggerRepository();
                    if (maybeRendererSupport instanceof RendererSupport)
                    {
                        rendererMap = ((RendererSupport)maybeRendererSupport).getRendererMap();
                    }
                    else
                    {
                        if (LOGGER.getLoggerRepository() instanceof RendererSupport)
                        {
                            rendererMap = ((RendererSupport)LOGGER.getLoggerRepository()).getRendererMap();
                        }
                    }
                }
                if (rendererMap == null && rendererSupport != null)
                {
                    rendererMap = rendererSupport.getRendererMap();
                }

                for (Iterator keys = mdcKeys.iterator(); keys.hasNext();)
                {
                    String nextKey = (String)keys.next();
                    Object nextValue = event.getMDC(nextKey);
                    String val = (rendererMap == null)
                        ? String.valueOf(nextValue)
                        : rendererMap.findAndRender(nextValue);
                    buf.append("<log4j:mdcEntry key=\"")
                        .append(Transform.escapeTags(nextKey))
                        .append("\" valueClass=\"")
                        .append(nextValue.getClass().getName())
                        .append("\" renderedValue=\"")
                        .append(Transform.escapeTags(val))
                        .append("\"/>\r\n");
                }

                buf.append("</log4j:mdc>\r\n");
                buf.append("</log4j:event>\r\n\r\n");
                formatted = buf.toString();
            }
        }
        return formatted;
    }
}

Monday, July 23, 2012

LMTPDelivery for JAMES 2.3 and DBmail 2.0

Years ago, I found the JAMES project and loved the idea of an open-source 100% Java email server. Then I found DBmail and loved the idea of an open-source implementation of a database-backed email storage system optimized for space, search, and maintenance. If the two could work together, it would be a great combination!

Alas, DBmail only accepted messages via pipe or LMTP and JAMES did not support either. The only way I could make the two work together would be to write integration code. Since LMTP is well-defined, the easiest route appeared to be to implement LMTP delivery for JAMES. It worked perfectly.

JAMES was at versions 2.2 and 2.3, and DBmail was at version 2.0 when I wrote this code. Both have advanced significantly since then. LMTPDelivery might not be useful in its current form, and for that matter JAMES might have implemented LMTP delivery too. However, since I found the code useful, I believe that makes it worth sharing. At least if I need it again, I'll know where to look!



package com.technicalabilities.mail.james.mailets;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.ParseException;

import org.apache.avalon.cornerstone.services.store.Store;
import org.apache.avalon.framework.configuration.DefaultConfiguration;
import org.apache.avalon.framework.container.ContainerUtil;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.james.Constants;
import org.apache.james.services.SpoolRepository;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
import org.apache.mailet.MailetContext;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.MatchResult;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;

import com.technicalabilities.mail.lmtp.LMTPAuthenticator;

/**
 * <p>
 * JAMES mailet for delivering a message to a LMTP server.
 * </p>
 * <p>
 * MThis class has liberally borrowed from the RemoteDelivery mailet in
 * JAMES 2.3 which is copyrighted by The Apache Software Foundation and licensed
 * under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License
 * version 2.0</a>.
 * </p>
 * <p>
 * To use, add to configuration in JAMES config.xml:
 *
 * <pre>
 *  &lt;mailet match=&quot;All&quot; class=&quot;com.technicalabilities.mail.james.mailets.LmtpDelivery&quot;&gt;
 *  &lt;debug&gt;true&lt;/debug&gt;
 *  &lt;host&gt;localhost&lt;/host&gt;
 *  &lt;port&gt;24&lt;/port&gt;
 *  &lt;lmtpSpool&gt; file://var/mail/lmtp/ &lt;/lmtpSpool&gt;
 *  &lt;!-- alternative database repository example below --&gt;
 *  &lt;!--
 *  &lt;lmtpSpool&gt; db://maildb/spool/lmtp &lt;/lmtpSpool&gt;
 *  --&gt;
 *
 *  &lt;!-- Delivery Schedule based upon RFC 2821, 4.5.4.1 --&gt;
 *  &lt;!-- 5 day retry period, with 4 attempts in the first
 *  hour, two more within the first 6 hours, and then
 *  every 6 hours for the rest of the period. --&gt;
 *  &lt;delayTime&gt;  5 minutes &lt;/delayTime&gt;
 *  &lt;delayTime&gt; 10 minutes &lt;/delayTime&gt;
 *  &lt;delayTime&gt; 45 minutes &lt;/delayTime&gt;
 *  &lt;delayTime&gt;  2 hours &lt;/delayTime&gt;
 *  &lt;delayTime&gt;  3 hours &lt;/delayTime&gt;
 *  &lt;delayTime&gt;  6 hours &lt;/delayTime&gt;
 *  &lt;maxRetries&gt; 25 &lt;/maxRetries&gt;
 *
 *  &lt;!-- The number of threads that should be trying to deliver messages --&gt;
 *  &lt;deliveryThreads&gt; 1 &lt;/deliveryThreads&gt;
 *
 *  &lt;!-- By default we send bounces to the &quot;bounce&quot; processor --&gt;
 *  &lt;!-- By removing this configuration James will fallback to hardcoded bounce --&gt;
 *  &lt;!-- notifications --&gt;
 *  &lt;bounceProcessor&gt;bounces&lt;/bounceProcessor&gt;
 *  &lt;/mailet&gt;
 *
 * </pre>
 *
 * </p>
 */
public class LmtpDelivery extends GenericMailet implements Runnable
{
    /**
     * Default delay time of 6 hours
     */
    private static final long DEFAULT_DELAY_TIME = 21600000;

    /**
     * Pattern string used to interpret the values in the delayTime
     * initialization parameters.
     *
     * @see #PATTERN
     */
    private static final String PATTERN_STRING = "\\s*([0-9]*\\s*[\\*])?\\s*([0-9]+)\\s*([a-z,A-Z]*)\\s*";

    /**
     * The compiled Perl pattern for PATTERN_STRING.
     *
     * @see #PATTERN_STRING
     */
    private static Pattern PATTERN = null;

    /**
     * String-to-millisecond multiplier for time units
     */
    private static final HashMap MULTIPLIERS = new HashMap(10);

    /**
     * Static initializer.
     * <p>
     * Compiles pattern for processing delaytime entries.
     * <p>
     * Initializes MULTIPLIERS with the supported unit quantifiers
     */
    static
    {
        try
        {
            Perl5Compiler compiler = new Perl5Compiler();
            PATTERN = compiler.compile(PATTERN_STRING, Perl5Compiler.READ_ONLY_MASK);
        }
        catch (MalformedPatternException mpe)
        {
            // this should not happen as the pattern string is hardcoded.
            System.err.println("Malformed pattern: " + PATTERN_STRING);
            mpe.printStackTrace(System.err);
        }
        // add allowed units and their respective multiplier
        MULTIPLIERS.put("msec", new Integer(1));
        MULTIPLIERS.put("msecs", new Integer(1));
        MULTIPLIERS.put("sec", new Integer(1000));
        MULTIPLIERS.put("secs", new Integer(1000));
        MULTIPLIERS.put("minute", new Integer(1000 * 60));
        MULTIPLIERS.put("minutes", new Integer(1000 * 60));
        MULTIPLIERS.put("hour", new Integer(1000 * 60 * 60));
        MULTIPLIERS.put("hours", new Integer(1000 * 60 * 60));
        MULTIPLIERS.put("day", new Integer(1000 * 60 * 60 * 24));
        MULTIPLIERS.put("days", new Integer(1000 * 60 * 60 * 24));
    }

    /**
     * This filter is used in the accept call to the spool. It will select the
     * next mail ready for processing according to the mails retrycount and
     * lastUpdated time
     */
    private class MultipleDelayFilter implements SpoolRepository.AcceptFilter
    {
        /**
         * holds the time to wait for the youngest mail to get ready for
         * processing
         */
        long youngest = 0;

        /**
         * Uses the getNextDelay to determine if a mail is ready for processing
         * based on the delivered parameters errorMessage (which holds the
         * retrycount), lastUpdated and state
         *
         * @param key
         *            the name/key of the message
         * @param state
         *            the mails state
         * @param lastUpdated
         *            the mail was last written to the spool at this time.
         * @param errorMessage
         *            actually holds the retrycount as a string (see failMessage
         *            below)
         */
        public boolean accept (String key, String state, long lastUpdated, String errorMessage)
        {
            if (state.equals(Mail.ERROR))
            {
                // Test the time...
                int retries = Integer.parseInt(errorMessage);

                // If the retries count is 0 we should try to send the mail now!
                if (retries == 0)
                    return true;

                long delay = getNextDelay(retries);
                long timeToProcess = delay + lastUpdated;

                if (System.currentTimeMillis() > timeToProcess)
                {
                    // We're ready to process this again
                    return true;
                }
                else
                {
                    // We're not ready to process this.
                    if (youngest == 0 || youngest > timeToProcess)
                    {
                        // Mark this as the next most likely possible mail to
                        // process
                        youngest = timeToProcess;
                    }
                    return false;
                }
            }
            else
            {
                // This mail is good to go... return the key
                return true;
            }
        }

        /**
         * @return the optimal time the SpoolRepository.accept(AcceptFilter)
         *         method should wait before trying to find a mail ready for
         *         processing again.
         */
        public long getWaitTime ()
        {
            if (youngest == 0)
            {
                return 0;
            }
            else
            {
                long duration = youngest - System.currentTimeMillis();
                youngest = 0; // get ready for next run
                return duration <= 0 ? 1 : duration;
            }
        }
    }

    /**
     * Controls certain log messages
     */
    private boolean isDebug = false;

    /**
     * The spool of LMTP-bound mail
     */
    private SpoolRepository lmtpSpool;

    /**
     * holds expanded delayTimes
     */
    private long [] delayTimes;

    /**
     * maximum number of retries
     */
    private int maxRetries = 5;

    /**
     * number of ms to timeout on lmtp delivery
     */
    private long lmtpTimeout = 180000;

    /**
     * number of delivery threads
     */
    private int deliveryThreadCount = 1;

    /**
     * the server to which to send all email
     */
    private String host = null;

    /**
     * the port where LMTP server is expected to be running
     */
    private int port;

    /**
     * the threads that will be handling the LMTP connection and protocol
     */
    private Collection deliveryThreads = new Vector();

    /**
     * Flag that the run method will check and end itself if set to true
     */
    private volatile boolean destroyed = false;

    /**
     * the processor for creating bounces
     */
    private String bounceProcessor = null;
   
    /**
     * matcher use at init time to parse delaytime parameters
     */
    private Perl5Matcher delayTimeMatcher;

    /**
     * used by accept to select the next mail ready for processing
     */
    private MultipleDelayFilter delayFilter = new MultipleDelayFilter();//

    /**
     * Initialize the mailet
     */
    public void init () throws MessagingException
    {
        isDebug = (getInitParameter("debug") == null) ? false : new Boolean(getInitParameter("debug")).booleanValue();
        ArrayList delay_times_list = new ArrayList();
        try
        {
            if (getInitParameter("delayTime") != null)
            {
                delayTimeMatcher = new Perl5Matcher();
                String delay_times = getInitParameter("delayTime");
                // split on comma's
                StringTokenizer st = new StringTokenizer(delay_times, ",");
                while (st.hasMoreTokens())
                {
                    String delay_time = st.nextToken();
                    delay_times_list.add(new Delay(delay_time));
                }
            }
            else
            {
                // use default delayTime.
                delay_times_list.add(new Delay());
            }
        }
        catch (Exception e)
        {
            log("Invalid delayTime setting: " + getInitParameter("delayTime"));
        }
        try
        {
            if (getInitParameter("maxRetries") != null)
            {
                maxRetries = Integer.parseInt(getInitParameter("maxRetries"));
            }
            // check consistency with delay_times_list attempts
            int total_attempts = calcTotalAttempts(delay_times_list);
            if (total_attempts > maxRetries)
            {
                log("Total number of delayTime attempts exceeds maxRetries specified. Increasing maxRetries from " + maxRetries + " to " + total_attempts);
                maxRetries = total_attempts;
            }
            else
            {
                int extra = maxRetries - total_attempts;
                if (extra != 0)
                {
                    log("maxRetries is larger than total number of attempts specified. Increasing last delayTime with " + extra + " attempts ");

                    if (delay_times_list.size() != 0)
                    {
                        Delay delay = (Delay)delay_times_list.get(delay_times_list.size() - 1); // last
                                                                                                // Delay
                        delay.setAttempts(delay.getAttempts() + extra);
                        log("Delay of " + delay.getDelayTime() + " msecs is now attempted: " + delay.getAttempts() + " times");
                    }
                    else
                    {
                        log("NO, delaytimes cannot continue");
                    }
                }
            }
            delayTimes = expandDelays(delay_times_list);

        }
        catch (Exception e)
        {
            log("Invalid maxRetries setting: " + getInitParameter("maxRetries"));
        }
        try
        {
            if (getInitParameter("timeout") != null)
            {
                lmtpTimeout = Integer.parseInt(getInitParameter("timeout"));
            }
        }
        catch (Exception e)
        {
            log("Invalid timeout setting: " + getInitParameter("timeout"));
        }

        bounceProcessor = getInitParameter("bounceProcessor");

        host = getInitParameter("host");

        String portNum = getInitParameter("port");
        try
        {
            port = (portNum == null) ? 24 : Integer.parseInt(portNum);
            if (port == 25)
            {
                throw new MessagingException("It is a violation of RFC 2033 for LMTP to be running on port 25 in order to prevent confusion with SMTP.");
            }
        }
        catch (NumberFormatException nfe)
        {
            throw (MessagingException)new MessagingException(portNum + " is not a number").initCause(nfe);
        }

        ServiceManager compMgr = (ServiceManager)getMailetContext().getAttribute(Constants.AVALON_COMPONENT_MANAGER);
        String outgoingPath = getInitParameter("lmtpSpool");
        if (outgoingPath == null)
        {
            outgoingPath = "file:///../var/mail/lmtp";
        }

        try
        {
            // Instantiate the a MailRepository for outgoing mails
            Store mailstore = (Store)compMgr.lookup(Store.ROLE);

            DefaultConfiguration spoolConf = new DefaultConfiguration("repository", "generated:LmtpDelivery.java");
            spoolConf.setAttribute("destinationURL", outgoingPath);
            spoolConf.setAttribute("type", "SPOOL");
            lmtpSpool = (SpoolRepository)mailstore.select(spoolConf);
        }
        catch (ServiceException cnfe)
        {
            log("Failed to retrieve Store component:" + cnfe.getMessage());
        }
        catch (Exception e)
        {
            log("Failed to retrieve Store component:" + e.getMessage());
        }

        // Start up a number of threads
        try
        {
            deliveryThreadCount = Integer.parseInt(getInitParameter("deliveryThreads"));
        }
        catch (Exception e)
        {}
        for (int i = 0; i < deliveryThreadCount; i++)
        {
            StringBuffer nameBuffer = new StringBuffer(32).append("Remote delivery thread (").append(i).append(")");
            Thread t = new Thread(this, nameBuffer.toString());
            t.start();
            deliveryThreads.add(t);
        }
    }

    /**
     * private method to log the extended SendFailedException introduced in JavaMail 1.3.2.
     */
    private void logSendFailedException (SendFailedException sfe)
    {
        if (isDebug)
        {
            log("Send failed: " + sfe.toString());
        }
    }

    /**
     * LMTPDelivery communicates with a single LMTP server using the LMTPTransport
     * implementation provided by Technical Abilities.
     *
     * @param mail the MimeMessage email to be delivered
     * @param session the JavaMail session to be used for this delivery
     * @return boolean whether the delivery was successful and the message can
     *         be deleted
     */
    private boolean deliver (Mail mail, Session session)
    {
        try
        {
            if (isDebug)
            {
                log("Attempting to deliver " + mail.getName());
            }
            MimeMessage message = mail.getMessage();

            // Create an array of the recipients as InternetAddress objects
            Collection recipients = mail.getRecipients();
            InternetAddress addr[] = new InternetAddress [recipients.size()];
            int j = 0;
            for (Iterator i = recipients.iterator(); i.hasNext(); j++)
            {
                MailAddress rcpt = (MailAddress)i.next();
                addr[j] = rcpt.toInternetAddress();
            }

            if (addr.length <= 0)
            {
                log("No recipients specified... not sure how this could have happened.");
                return true;
            }

            Properties props = session.getProperties();
            if (mail.getSender() == null)
            {
                props.put("mail.lmtp.from", "<>");
            }
            else
            {
                String sender = mail.getSender().toString();
                props.put("mail.lmtp.from", sender);
            }

            Transport transport = session.getTransport("lmtp");
            transport.connect(host, port, null, null);

            try
            {
                transport.sendMessage(message, addr);
            }
            finally
            {
                if (transport != null)
                {
                    transport.close();
                    transport = null;
                }
            }

            StringBuffer logMessageBuffer = new StringBuffer(256).append("Mail (").append(mail.getName()).append(") sent successfully to ").append(host).append(":").append(port).append(" for ").append(mail.getRecipients());
            log(logMessageBuffer.toString());
            return true;
        }
        catch (SendFailedException sfe)
        {
            logSendFailedException(sfe);

            Collection recipients = mail.getRecipients();

            boolean deleteMessage = false;

            /*
             * If you send a message that has multiple invalid addresses, you'll
             * get a top-level SendFailedException that that has the valid,
             * valid-unsent, and invalid address lists, with all of the server
             * response messages will be contained within the nested exceptions.
             * [Note: the content of the nested exceptions is implementation
             * dependent.] sfe.getInvalidAddresses() should be considered
             * permanent. sfe.getValidUnsentAddresses() should be considered
             * temporary. JavaMail v1.3 properly populates those collections
             * based upon the 4xx and 5xx response codes to RCPT TO. Some
             * servers, such as Yahoo! don't respond to the RCPT TO, and provide
             * a 5xx reply after DATA. In that case, we will pick up the failure
             * from SMTPSendFailedException.
             */

            // log the original set of intended recipients
            if (isDebug)
                log("Recipients: " + recipients);

            if (sfe.getInvalidAddresses() != null)
            {
                Address [] address = sfe.getInvalidAddresses();
                if (address.length > 0)
                {
                    recipients.clear();
                    for (int i = 0; i < address.length; i++)
                    {
                        try
                        {
                            recipients.add(new MailAddress(address[i].toString()));
                        }
                        catch (ParseException pe)
                        {
                            // this should never happen ... we should have
                            // caught malformed addresses long before we
                            // got to this code.
                            log("Can't parse invalid address: " + pe.getMessage());
                        }
                    }
                    if (isDebug)
                        log("Invalid recipients: " + recipients);
                    deleteMessage = failMessage(mail, sfe, true);
                }
            }

            if (sfe.getValidUnsentAddresses() != null)
            {
                Address [] address = sfe.getValidUnsentAddresses();
                if (address.length > 0)
                {
                    recipients.clear();
                    for (int i = 0; i < address.length; i++)
                    {
                        try
                        {
                            recipients.add(new MailAddress(address[i].toString()));
                        }
                        catch (ParseException pe)
                        {
                            // this should never happen ... we should have
                            // caught malformed addresses long before we
                            // got to this code.
                            log("Can't parse unsent address: " + pe.getMessage());
                        }
                    }
                    if (isDebug)
                        log("Unsent recipients: " + recipients);
                    deleteMessage = failMessage(mail, sfe, false);
                }
            }

            return deleteMessage;
        }
        catch (MessagingException ex)
        {
            // We should do a better job checking this... if the failure is a
            // general
            // connect exception, this is less descriptive than more specific
            // SMTP command
            // failure... have to lookup and see what are the various Exception
            // possibilities

            // Unable to deliver message after numerous tries... fail
            // accordingly

            // We check whether this is a 5xx error message, which
            // indicates a permanent failure (like account doesn't exist
            // or mailbox is full or domain is setup wrong).
            // We fail permanently if this was a 5xx error
            return failMessage(mail, ex, ex.getMessage() != null && ('5' == ex.getMessage().charAt(0)));
        }
    }

    /**
     * An email delivery failed.  File for future attempts or bounce as appropriate.
     *
     * @param mail the email that failed to be delivered
     * @param exception the exception that cause this failure
     * @param boolean whether or not the failure was permanent
     * @return boolean Whether the message failed fully and can be deleted
     */
    private boolean failMessage (Mail mail, MessagingException ex, boolean permanent)
    {
        StringWriter sout = new StringWriter();
        PrintWriter out = new PrintWriter(sout, true);
        if (permanent)
        {
            out.print("Permanent");
        }
        else
        {
            out.print("Temporary");
        }
        StringBuffer logBuffer = new StringBuffer(64).append(" exception delivering mail (").append(mail.getName()).append(": ");
        out.print(logBuffer.toString());
        if (isDebug)
            ex.printStackTrace(out);
        log(sout.toString());
        if (!permanent)
        {
            if (!mail.getState().equals(Mail.ERROR))
            {
                mail.setState(Mail.ERROR);
                mail.setErrorMessage("0");
                mail.setLastUpdated(new Date());
            }
            int retries = Integer.parseInt(mail.getErrorMessage());
            if (retries < maxRetries)
            {
                logBuffer = new StringBuffer(128).append("Storing message ").append(mail.getName()).append(" into lmtp after ").append(retries).append(" retries");
                log(logBuffer.toString());
                ++retries;
                mail.setErrorMessage(retries + "");
                mail.setLastUpdated(new Date());
                return false;
            }
            else
            {
                logBuffer = new StringBuffer(128).append("Bouncing message ").append(mail.getName()).append(" after ").append(retries).append(" retries");
                log(logBuffer.toString());
            }
        }

        if (mail.getSender() == null)
        {
            log("Null Sender: no bounce will be generated for " + mail.getName());
            return true;
        }

        if (bounceProcessor != null)
        {
            // do the new DSN bounce
            // setting attributes for DSN mailet
            mail.setAttribute("delivery-error", ex);
            mail.setState(bounceProcessor);
            // re-insert the mail into the spool for getting it passed to the
            // dsn-processor
            MailetContext mc = getMailetContext();
            try
            {
                mc.sendMail(mail);
            }
            catch (MessagingException e)
            {
                // we shouldn't get an exception, because the mail was already
                // processed
                log("Exception re-inserting failed mail: ", e);
            }
        }
        else
        {
            // do an old style bounce
            bounce(mail, ex);
        }
        return true;
    }

    /**
     * Bounce an email back to the sender(s)
     *
     * @param mail the email that is being bounced
     * @param ex the exception that caused the bounce
     */
    private void bounce (Mail mail, MessagingException ex)
    {
        StringWriter sout = new StringWriter();
        PrintWriter out = new PrintWriter(sout, true);
        String machine = "[unknown]";
        try
        {
            InetAddress me = InetAddress.getLocalHost();
            machine = me.getHostName();
        }
        catch (Exception e)
        {
            machine = "[address unknown]";
        }
        StringBuffer bounceBuffer = new StringBuffer(128).append("Hi. This is the James mail server at ").append(machine).append(".");
        out.println(bounceBuffer.toString());
        out.println("I'm afraid I wasn't able to deliver your message to the following addresses.");
        out.println("This is a permanent error; I've given up. Sorry it didn't work out.  Below");
        out.println("I include the list of recipients and the reason why I was unable to deliver");
        out.println("your message.");
        out.println();
        for (Iterator i = mail.getRecipients().iterator(); i.hasNext();)
        {
            out.println(i.next());
        }
        if (ex.getNextException() == null)
        {
            out.println(ex.getMessage().trim());
        }
        else
        {
            Exception ex1 = ex.getNextException();
            if (ex1 instanceof SendFailedException)
            {
                out.println("Remote mail server told me: " + ex1.getMessage().trim());
            }
            else if (ex1 instanceof UnknownHostException)
            {
                out.println("Unknown host: " + ex1.getMessage().trim());
                out.println("This could be a DNS server error, a typo, or a problem with the recipient's mail server.");
            }
            else if (ex1 instanceof ConnectException)
            {
                // Already formatted as "Connection timed out: connect"
                out.println(ex1.getMessage().trim());
            }
            else if (ex1 instanceof SocketException)
            {
                out.println("Socket exception: " + ex1.getMessage().trim());
            }
            else
            {
                out.println(ex1.getMessage().trim());
            }
        }
        out.println();

        log("Sending failure message " + mail.getName());
        try
        {
            getMailetContext().bounce(mail, sout.toString());
        }
        catch (MessagingException me)
        {
            log("Encountered unexpected messaging exception while bouncing message: " + me.getMessage());
        }
        catch (Exception e)
        {
            log("Encountered unexpected exception while bouncing message: " + e.getMessage());
        }
    }

    public String getMailetInfo ()
    {
        return "RemoteDelivery Mailet";
    }

    /**
     * For this message, we take the list of recipients, organize these into
     * distinct servers, and duplicate the message for each of these servers,
     * and then call the deliver (messagecontainer) method for each
     * server-specific messagecontainer ... that will handle storing it in the
     * lmtp queue if needed.
     *
     * @param mail
     *            org.apache.mailet.Mail
     */
    public void service (Mail mail) throws MessagingException
    {
        // Do I want to give the internal key, or the message's Message ID
        if (isDebug)
        {
            StringBuffer logMessageBuffer = new StringBuffer(128).append("Sending mail ").append(mail.getName()).append("via LMTP to ").append(mail.getRecipients()).append(" at ").append(host).append(":").append(port);
            log(logMessageBuffer.toString());
        }

        // Set it to try to deliver (in a separate thread) immediately
        // (triggered by storage)
        lmtpSpool.store(mail);
        mail.setState(Mail.GHOST);
    }

    // Need to synchronize to get object monitor for notifyAll()
    public synchronized void destroy ()
    {
        // Mark flag so threads from this mailet stop themselves
        destroyed = true;
        // Wake up all threads from waiting for an accept
        for (Iterator i = deliveryThreads.iterator(); i.hasNext();)
        {
            Thread t = (Thread)i.next();
            t.interrupt();
        }
        notifyAll();
    }

    /**
     * Handles checking the lmtp spool for new mail and delivering them if there
     * are any
     */
    public void run ()
    {

        /*
         * TODO: CHANGE ME!!! The problem is that we need to wait for James to
         * finish initializing. We expect the HELLO_NAME to be put into the
         * MailetContext, but in the current configuration we get started before
         * the SMTP Server, which establishes the value. Since there is no
         * contractual guarantee that there will be a HELLO_NAME value, we can't
         * just wait for it. As a temporary measure, I'm inserting this
         * philosophically unsatisfactory fix.
         */
        long stop = System.currentTimeMillis() + 60000;
        while ((getMailetContext().getAttribute(Constants.HELLO_NAME) == null) && stop > System.currentTimeMillis())
        {
            try
            {
                Thread.sleep(1000);
            }
            catch (Exception ignored)
            {} // wait for James to finish initializing
        }

        // Checks the pool and delivers a mail message
        Properties props = new Properties();
        // Not needed for production environment
        props.put("mail.debug", "true");
        // Sets timeout on going connections
        props.put("mail.lmtp.timeout", lmtpTimeout + "");
        if (host != null)
        {
            props.put("mail.lmtp.host", host);
        }
        if (port > 0)
        {
            props.put("mail.lmtp.port", String.valueOf(port));
        }

        Session session = Session.getInstance(props, new LMTPAuthenticator());
        try
        {
            while (!Thread.interrupted() && !destroyed)
            {
                try
                {
                    Mail mail = (Mail)lmtpSpool.accept(delayFilter);
                    String key = mail.getName();
                    try
                    {
                        if (isDebug)
                        {
                            StringBuffer logMessageBuffer = new StringBuffer(128).append(Thread.currentThread().getName()).append(" will process mail ").append(key);
                            log(logMessageBuffer.toString());
                        }
                        if (deliver(mail, session))
                        {
                            // Message was successfully delivered/fully
                            // failed... delete it
                            ContainerUtil.dispose(mail);
                            lmtpSpool.remove(key);
                        }
                        else
                        {
                            // Something happened that will delay delivery.
                            // Store any updates
                            lmtpSpool.store(mail);
                            ContainerUtil.dispose(mail);
                            // This is an update, we have to unlock and notify
                            // or this mail
                            // is kept locked by this thread
                            lmtpSpool.unlock(key);
                            // We do not notify because we updated an already
                            // existing mail
                            // and we are now free to handle more mails.
                            // Furthermore this mail should not be processed now
                            // because we
                            // have a retry time scheduling.
                        }
                        // Clear the object handle to make sure it recycles this
                        // object.
                        mail = null;
                    }
                    catch (Exception e)
                    {
                        // Prevent unexpected exceptions from causing looping by
                        // removing
                        // message from lmtp.
                        // DO NOT CHNANGE THIS to catch Error! For example, if
                        // there were an OutOfMemory condition
                        // caused because something else in the server was
                        // abusing memory, we would not want to
                        // start purging the lmtp spool!
                        ContainerUtil.dispose(mail);
                        lmtpSpool.remove(key);
                        throw e;
                    }
                }
                catch (Throwable e)
                {
                    if (!destroyed)
                        log("Exception caught in LmtpDelivery.run()", e);
                }
            }
        }
        finally
        {
            // Restore the thread state to non-interrupted.
            Thread.interrupted();
        }
    }

    /**
     * @param list
     *            holding Delay objects
     * @return the total attempts for all delays
     */
    private int calcTotalAttempts (ArrayList list)
    {
        int sum = 0;
        Iterator i = list.iterator();
        while (i.hasNext())
        {
            Delay delay = (Delay)i.next();
            sum += delay.getAttempts();
        }
        return sum;
    }

    /**
     * This method expands an ArrayList containing Delay objects into an array
     * holding the only delaytime in the order.
     * <p>
     * So if the list has 2 Delay objects the first having attempts=2 and
     * delaytime 4000 the second having attempts=1 and delaytime=300000 will be
     * expanded into this array:
     * <p>
     * long[0] = 4000
     * <p>
     * long[1] = 4000
     * <p>
     * long[2] = 300000
     * <p>
     *
     * @param list
     *            the list to expand
     * @return the expanded list
     */
    private long [] expandDelays (ArrayList list)
    {
        long [] delays = new long [calcTotalAttempts(list)];
        Iterator i = list.iterator();
        int idx = 0;
        while (i.hasNext())
        {
            Delay delay = (Delay)i.next();
            for (int j = 0; j < delay.getAttempts(); j++)
            {
                delays[idx++] = delay.getDelayTime();
            }
        }
        return delays;
    }

    /**
     * This method returns, given a retry-count, the next delay time to use.
     *
     * @param retry_count
     *            the current retry_count.
     * @return the next delay time to use, given the retry count
     */
    private long getNextDelay (int retry_count)
    {
        if (retry_count > delayTimes.length)
        {
            return DEFAULT_DELAY_TIME;
        }
        return delayTimes[retry_count - 1];
    }

    /**
     * @return "LMTPDelivery"
     */
    public String getMailetName ()
    {
        return "LMTPDelivery";
    }
   
    /**
     * This class is used to hold a delay time and its corresponding number of
     * retries.
     */
    private class Delay
    {
        private int attempts = 1;

        private long delayTime = DEFAULT_DELAY_TIME;

        /**
         * This constructor expects Strings of the form
         * "[attempt\*]delaytime[unit]".
         * <p>
         * The optional attempt is the number of tries this delay should be used
         * (default = 1) The unit if present must be one of
         * (msec,sec,minute,hour,day) (default = msec) The constructor
         * multiplies the delaytime by the relevant multiplier for the unit, so
         * the delayTime instance variable is always in msec.
         *
         * @param init_string
         *            the string to initialize this Delay object from
         */
        public Delay (String init_string) throws MessagingException
        {
            String unit = "msec"; // default unit
            if (delayTimeMatcher.matches(init_string, PATTERN))
            {
                MatchResult res = delayTimeMatcher.getMatch();
                // the capturing groups will now hold
                // at 1: attempts * (if present)
                // at 2: delaytime
                // at 3: unit (if present)

                if (res.group(1) != null && !res.group(1).equals(""))
                {
                    // we have an attempt *
                    String attempt_match = res.group(1);
                    // strip the * and whitespace
                    attempt_match = attempt_match.substring(0, attempt_match.length() - 1).trim();
                    attempts = Integer.parseInt(attempt_match);
                }

                delayTime = Long.parseLong(res.group(2));

                if (!res.group(3).equals(""))
                {
                    // we have a unit
                    unit = res.group(3).toLowerCase(Locale.US);
                }
            }
            else
            {
                throw new MessagingException(init_string + " does not match " + PATTERN_STRING);
            }
            if (MULTIPLIERS.get(unit) != null)
            {
                int multiplier = ((Integer)MULTIPLIERS.get(unit)).intValue();
                delayTime *= multiplier;
            }
            else
            {
                throw new MessagingException("Unknown unit: " + unit);
            }
        }

        /**
         * This constructor makes a default Delay object, ie. attempts=1 and
         * delayTime=DEFAULT_DELAY_TIME
         */
        public Delay ()
        {}

        /**
         * @return the delayTime for this Delay
         */
        public long getDelayTime ()
        {
            return delayTime;
        }

        /**
         * @return the number attempts this Delay should be used.
         */
        public int getAttempts ()
        {
            return attempts;
        }

        /**
         * Set the number attempts this Delay should be used.
         */
        public void setAttempts (int value)
        {
            attempts = value;
        }

        /**
         * Pretty prints this Delay
         */
        public String toString ()
        {
            StringBuffer buf = new StringBuffer(15);
            buf.append(getAttempts());
            buf.append('*');
            buf.append(getDelayTime());
            buf.append("msec");
            return buf.toString();
        }
    }
}



The referenced LMTPAuthenticator class is a no-op subclass of Authenticator:

package com.technicalabilities.mail.lmtp;

import javax.mail.Authenticator;

public class LMTPAuthenticator extends Authenticator
{
}