Friday, November 13, 2015

Arrays

Don't reinvent the wheel when there is a racecar available for free!

I have the amusing difficulty of having been in Java too long. Back in version 1.0, I was writing my own serialization mechanism because none existed, however in version 1.1 Java serialization was introduced. In 1.1 and 1.2, I was working on the Voyager ORB where we were generating proxy classes on the fly. Lo and behold, in version 1.3 dynamic proxy generation was introduced.

The Java Core APIs and Java Extension APIs now have many capabilities that I am still realizing are there. I find that solving these small problems can be personally satisfying, but I still would rather have used one already available. I will document some of the useful Java Core APIs that I like to use.

Beginning with:

java.util.Arrays

The java.util.Arrays class has really grown up. Many methods are overloaded to support every primitive type as well as Java Generics parameterized Object references. In these cases, TYPE is used as a placeholder to indicate there is a method for every such type.

public static <T> List<T> asList(T... a)

Create a List front-end to an array. Any actions taken on items in the List affect the Objects in the underlying Array.


    public static void main (String [] args)
    {
        List<String> list = Arrays.asList(args);

    }

Since a java.util.List can only hold Object references and not primitive types, there are no primitive variants for this method.

public static boolean equals(TYPE [] a, TYPE [] a2)

Performs a shallow equals on the two arrays. For example, for primitive types the algorithm would be like:

return (a[0] == a2[0] && a[1] == a2[1] && ...)

whereas for Object types (assuming no null references):

return (a[0].equals(a2[0]) && a[1].equals(a2[1]) && .... )

public static boolean deepEquals(Object [] aObject [] a2)
public int deepHashCode(Object [] a)
public String deepToString(Object[] a)

The deep* methods recursively step into any array elements that are themselves arrays.

deepEquals() performs a deep equals on the two arrays. That is, if an element in the arrays is yet another array, then the contents of those arrays is recursively compared via deepEquals.

deepHashCode() calculates a hashCode value for the array by stepping into its contents and the contents of any arrays contained therein.

deepToString() creates a String value for the array from the String values of the array's contents and the contents of any arrays contained therein.

public static void sort (TYPE [] array, int fromIndex, int toIndex)
public static void sort (T[] array, Comparator<? super T> c)
public static void sort (T[] array, int fromIndx, int toIndex, Comparator<? super T> c)

The sort() methods perform an in-place quicksort on the given array. If the indices are given, then the sort is only performed between those indices. For Object references, a Comparator can be given. Once an array is sorted, the corresponding binarySearch() method can be used to efficiently search the data in the array.

Other methods include functions to copy part or all of the contents of an array and to fill an array. There area also methods to process the elements of an array in parallel for various behaviors including sort and set.

Monday, November 2, 2015

Accidentally

It was an accident. I swear! I got into Java because my manager had heard a new buzzword: "Java", and she wanted someone to look into it. I just happened to be that someone she chose, so off researching I went. I was just a year out of college. The latest Java version was 1.0.2. Server-side Java applications did not exist. All GUI actions went through the Component.action() method. Applets were ALL the rage. So Applets it had to be. I wrote an Applet corollary to one or our Xwindows apps. The only piece of functionality I added was the ability to change the font type and size for the display. As luck would have it, that was the feature that our customers LOVED! From there I became the Java Guy and several other apps were targeted for Java redesigns. That led to job offers doing even more Java work for much higher pay. Yes, this was me:
Today, I can now claim I have been doing Java full time for over 18 years. I will admit, I'm still learning the new additions introduced in JSE 1.8. I will get there. If for no other reason, then because I will have to fix a bug in code someone wrote using the newest Java features. Or in other words, by accident.

This blog entry also appears at Quantum Strides.

Thursday, January 23, 2014

The Method To My Madness

Just about everyone has a preferred coding style. It generally manifests in formatting choices. For example, although the Java standard for braces is to have open braces at the end of a line, I refused to follow that standard. My reason is because I actually learned C and C++ before Java came on the scene, and I learned to format my C and C++ code with braces on their own lines and indented in pairs. As a result, I learned to pair braces according to their matched indentation, and that familiarity is why I continue to do so. Sure, I sometimes get remarks from other Java programmers at times, and I have occasionally clashed with official company styles. To them I say: let me do what I know; I am good at it! To be fair, of course, I never demand a particular style from anyone else either -- though reviewing the code of someone that is inconsistent in their own style will get complaints from me. Be consistent!

There is another aspect to my coding style which gets a lot more questions, and that is because it is more than just simple formatting: I write my code specifically to avoid break statements, continue statements, and nested returns. As an example, a double nested loop in my code will resemble:

boolean closed = false;

for (int i=0; !closed && i < fooList.size(); ++i)
{
    List<Bar> barList = foolist.get(i).getBarList();
    for (int j=0; !closed && j < barList.size(); ++j)
    {
        closed = barList.get(j).isClosed();
    }
}

return closed;

Colleagues reviewing my code often ask why I don't just use the nested return such as:

for (Foo foo : fooList)
{
    for (Bar bar : foo.getBarList())
    {
        if (bar.isClosed())
        {
            return true;
        }
    }
}

return false;

This is my answer:

The looping style I use is related to what I learned in one of my graduate-level courses in Computer Engineering when we studied formal proofs of computer programs. I happen to be very mathematically minded and even got a minor in Mathematics as an undergrad, so this particular subject appealed to me. I was also curious how IBM had done some significant development with software validation via formal proofs and no testing and found the results to be at least as high in quality (fewer bugs) as software developed with testing.

Formally proving the correctness of a loop requires stating the invariant of that loop and the condition of the loop. The way I write the condition in the for loop, the invariant and condition can be easily determined by examination. In this case the outer loop invariant is i <= fooList.size() and the condition is as written: !closed && i < fooList.size(). The inner loop is similar.

Since the nested return is logically equivalent, the invariants and conditions are the same, but determining them from examination is extremely difficult. I do not expect anyone to formally prove my loop, but if desired it could be. For myself, I find it easier to validate my style when reviewing code since I tend to think more mathematically.

That is how I got to writing my loops this way. After I had been doing this a short while, I discovered a particular advantage that is even more appealing to me: the single exit point. By having a single exit point, I have a single clear location where I can set a breakpoint or insert a log statement when debugging. I also have an obvious variable to watch (closed in this case). I also have a clear delineation for the beginning and ending of a try block in the event that an exception needs to be caught. This has led me to write my if statements without nested returns as well. All in all, it feels more disciplined to me.

Since this style has paid such dividends to me over the years, I continue to use it. That’s why you don’t find nested return, break, or continue statements in my code. I don’t usually mind if others use them — unless I see a continue statement with a label then I do complain rather loudly.

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
{
}