Advanced java code dealing with real world problems.

Monday, November 30, 2009

Use wss4j with Axis1.4 for message encryption and signing

Use wss4j with Axis1.4 for message encryption and signing.

Axis 1.4 and wss4j have been out for quite a while, but the detailed documentations about using them together is hard to find. Detailed documentations are abundant for using Rampant with Axis 2. But if you are like me stuck to the Axis 1 and want to encrypt and sign your messages without modifying your existing code, wss4j is still the best option out there.

The Apache wss4j web site provides some wonderful documentations about Axis deployment tutorial and samples. The hard part is how to put everything together. What I am about to offer here is the detailed steps about putting everything together so that you can implement the message encryption and signing with your existing web services using wss4j with Axis 1.4.

I will take a different approach here, let's tackle the key stores first.

To be able to handle encrypted messages, you'll need a pair of keys, a public key that is used to encrypt messages which the client will use, and a private key that is used to decrypt messages which you keep it safe in your server.  To generate self-signed key stores using java keytool, issue following command in a dos window or a shell prompt:

keytool -genkey -dname "CN=Server, OU=Encryption, O=JacksBlog, L=Raleigh, S=NC, C=US" -alias serverkey -keypass serverpass -validity 9999 -keyalg RSA -sigalg SHA1withRSA -keystore server.keystore -storepass nosecret

This will create a file called server.keystore which contains a private and public key pair for encryption purpose.

Next we will create a key pair for message signing:

keytool -genkey -dname "CN=Client, OU=Signing, O=JacksBlog, L=Raleigh, S=NC, C=US" -alias clientkey -keypass clientpass -validity 9999 -keyalg RSA -sigalg SHA1withRSA -keystore client.keystore -storepass nosecret

This will create a file called client.keystore that contains a key pair for message signing.

In order for the client to trust the server, we need to export the public key from server.keystore and import it to client.keystore:

keytool -export -alias serverkey -keystore server.keystore -storepass nosecret -file servercert.cer

keytool -import -alias serverkey -keystore client.keystore -storepass nosecret -file servercert.cer

In order for the server to trust the client, we need to export the public key from client.keystore and import it to server.keystore:

keytool -export -alias clientkey -keystore client.keystore -storepass nosecret -file clientcert.cer

keytool -import -alias clientkey -keystore server.keystore -storepass nosecret -file clientcert.cer

Now we are ready for Axis configurations, please refer to wss4j web site for how to install wss4j on Axis 1.4. I'll highlight a couple of key points here:

1) Download the binary distribution from Apache wss4j web site, and unzip it into a folder. Make sure to read through the README.txt file, and download all the required jar files listed in the README.txt file.

2) Copy all the required jar files along with wss4j jar file to your Axis's WEB-INF/lib directory.

Now the detailed steps:

3) Create a password callback class in your Axis project and compile it to your class path:

public class PasswordCallBackHandler implements CallbackHandler {
    public void handle(Callback[] callbacks) throws IOException {
        for (int i = 0; i < callbacks.length; i++) {
            WSPasswordCallback pwcb = (WSPasswordCallback)callbacks[i];
            String id = pwcb.getIdentifier();
            int usage = pwcb.getUsage();
            if (usage == WSPasswordCallback.DECRYPT || usage == WSPasswordCallback.SIGNATURE) {
                // used to retrieve password for private key
                if ("serverkey".equals(id)) {
                    pwcb.setPassword("serverpass");
                }
                else if ("clientkey".equals(id)) {
                    pwcb.setPassword("clientpass");
                }
            }
         }
    }
}

4) Create a folder called Keys under WEB-INF/classes and copy the server.keystore file to the folder.

5) Create a crypto.properties file and copy it to WEB-INF/classes folder:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=nosecret
org.apache.ws.security.crypto.merlin.keystore.alias=serverkey
org.apache.ws.security.crypto.merlin.file=Keys/server.keystore

6) Pick up an existing web service from your Axis project (or simply create a new one) that you want to have messages encrypted and signed, and add the required entries to your server-config.wsdd file. For example I picked up a service call MyService and added "requestFlow" entry into the wsdd:

    <service name="MyService" provider="java:MSG">
        <requestFlow>
            <handler type="java:org.apache.ws.axis.security.WSDoAllReceiver">
                <parameter name="action" value="Signature Encrypt"/>
                <parameter name="signaturePropFile" value="./WEB-INF/classes/crypto.properties" />
                 <parameter name="passwordCallbackClass" value="PasswordCallBackHandler"/>
            </handler>
          </requestFlow>
        <parameter name="className" value="blog.MyService" />
        <parameter name="allowedMethods" value="process" />
    </service>

7) Restart your server and the MyService should be ready to serve encrypted and signed service calls.

Now let's modify the client code to call the service.

1) Copy the client.keystore file to the root path of your client project.

2) Create a crypto.properties file and copy it to the root class path:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=nosecret
org.apache.ws.security.crypto.merlin.keystore.alias=clientkey
org.apache.ws.security.crypto.merlin.file=client.keystore

3) Create a wsdd file called client_deploy.wsdd and copy it to the root class path (please change the "passwordCallbackClass" value accordingly):

<deployment xmlns="http://xml.apache.org/axis/wsdd/"
    xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
    <transport name="http" pivot="java:org.apache.axis.transport.http.HTTPSender" />
    <globalConfiguration>
        <requestFlow>
            <handler type="java:org.apache.ws.axis.security.WSDoAllSender">
                <parameter name="user" value="clientkey" />

                <parameter name="encryptionUser" value="serverkey"/>
                <parameter name="action" value="Signature Encrypt" />
                <parameter name="signaturePropFile" value="crypto.properties" />
                <parameter name="passwordCallbackClass" value="blog.ServiceClient" />
            </handler>
        </requestFlow>
    </globalConfiguration>
 </deployment>
4) Modify the client code, add this line before the service call is initialized:

        System.setProperty("axis.ClientConfigFile", "client_deploy.wsdd");

5) Add the following method to your client code, make sure the client class implements CallbackHandler:

    public void handle(Callback[] callbacks) throws IOException {
        for (int i = 0; i < callbacks.length; i++) {
            WSPasswordCallback pwcb = (WSPasswordCallback)callbacks[i];
            String id = pwcb.getIdentifier();
            int usage = pwcb.getUsage();
            if (usage == WSPasswordCallback.DECRYPT || usage == WSPasswordCallback.SIGNATURE) {
                // used to retrieve password for private key
                if ("clientkey".equals(id)) {
                    pwcb.setPassword("clientpass");
                }
            }
        }
    }

That should be it. Compile and run your client class and keep your fingers crossed.

Thursday, September 10, 2009

Build a reliable email reader - Part 1

When I was building a back-end email reader that reads emails from a mailbox and save them for future processing. I encountered a problem that the back-end task would stop unexpectedly due to various reasons, such as when a malformed email was received, or the mailbox was temporary disconnected, or the pop3 or imap server was temporarily out of service for maintainance, etc.

A reliable email reader program was needed so it can be started to run continually until some catastrophic events occurred. The email reader presented here will read email messages into our portable message beans, and display them to the console. The pop3 server located on the "localhost" is used, and the mailbox name is "support" with password "support". Please change them to point to your pop3 account accordingly.

We will need two more supporting classes which I will present them in Part 2.
/*
 * blog/javaclue/javamail/MailReader.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package blog.javaclue.javamail;

import java.io.IOException;
import java.util.Date;
import java.util.Properties;

import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.event.ConnectionEvent;
import javax.mail.event.ConnectionListener;
import javax.mail.event.MessageCountAdapter;
import javax.mail.event.MessageCountEvent;
import javax.mail.event.StoreEvent;
import javax.mail.event.StoreListener;

import org.apache.log4j.Logger;

/**
 * This class provides methods to read e-mails from a mailbox.
 */
public class MailReader implements ConnectionListener, StoreListener {
 private static final long serialVersionUID = -9061869821061961065L;
 private static final Logger logger = Logger.getLogger(MailReader.class);
 protected static final boolean isDebugEnabled = logger.isDebugEnabled();

 protected final String LF = System.getProperty("line.separator", "\n");
 private final boolean debugSession = false;

 private final Session session;
 private final Mailbox mailbox;
 private final MailProcessor processor;
 
 private Store store = null;
 private Folder folder = null;
 
 private static final int MAX_MSGS_PER_READ = 100;
 private static final int MAX_WAIT = 120 * 1000; // up to two minutes

 private final int msgsPerRead;
 private final int pollingFreq;
 private int messagesProcessed = 0;

 private static final int[] RetryFreqs = 
  { 5, 10, 10, 20, 20, 20, 30, 30, 30, 30, 60, 60, 60, 60, 60 }; // in seconds
 private static final int RETRY_FREQ = 120; // in seconds

 public static void main(String[] args) {
  Mailbox vo = new Mailbox("localhost", "support", "support");
  MailReader reader = new MailReader(vo);
  try {
   reader.readMail();
  }
  catch (Exception e) {
   e.printStackTrace();
  }
 }

 /**
  * create a MailReader instance
  * 
  * @param mbox -
  *            mailbox properties
  */
 public MailReader(Mailbox mbox) {
  this.mailbox = mbox;

  // number of e-mails (="msgsPerPass") to read per cycle
  int msgs_per_read = mbox.getMessagesPerRead();
  msgs_per_read = msgs_per_read <= 0 ? 5 : msgs_per_read; // default is 5
  msgsPerRead = msgs_per_read;

  // number of seconds (="pollingFreq") to wait between reads
  int _freq = mbox.getMinimumWait() * 1000 + msgsPerRead * 100;
  pollingFreq = _freq > MAX_WAIT ? MAX_WAIT : _freq; // upper limit is MAX_WAIT
  if (isDebugEnabled)
   logger.debug("Wait between reads in milliseconds: " + pollingFreq);
  
  // enable RFC2231 support in parameter lists, since javamail 1.4
  // Since very few existing programs support RFC2231, disable it for now
  /*
  System.setProperty("mail.mime.encodeparameters", "true");
  System.setProperty("mail.mime.decodeparameters", "true");
  System.setProperty("mail.mime.encodefilename", "true");
  System.setProperty("mail.mime.decodefilename", "true");
  */
  
  // to make the reader more tolerable
  System.setProperty("mail.mime.multipart.ignoremissingendboundary", "true");
  System.setProperty("mail.mime.multipart.ignoremissingboundaryparameter", "true");
  
  Properties m_props = (Properties) System.getProperties().clone();
  m_props.setProperty("mail.debug", "true");
  m_props.setProperty("mail.debug.quote", "true");

  /*
   * POP3 - properties of com.sun.mail.pop3 
   * mailbox can be accessed via URL: pop3://user:password@host:port/INBOX
   */
  // set timeouts in milliseconds. default for both is infinite
  // Socket connection timeout
  m_props.setProperty("mail.pop3.connectiontimeout", "900000");
  // Socket I/O timeout
  m_props.setProperty("mail.pop3.timeout", "750000");
  // m_props.setProperty("mail.pop3.rsetbeforequit","true");
  /* issue RSET before QUIT, default: false */

  /* IMAP - properties of com.sun.mail.imap */
  // set timeouts in milliseconds. default for both is infinite
  // Socket connection timeout
  m_props.setProperty("mail.imap.connectiontimeout", "900000");
  // Socket I/O timeout
  m_props.setProperty("mail.imap.timeout", "750000");
  
  // Certain IMAP servers do not implement the IMAP Partial FETCH
  // functionality properly
  // set Partial fetch to false to workaround exchange server 5.5 bug
  m_props.setProperty("mail.imap.partialfetch","false");
  
  // If your version of Exchange doesn't implement POP3 properly, you need
  // to tell JavaMail to forget about TOP headers by setting the 
  // mail.pop3.forgettopheaders property to true.
  if (mbox.isExchange()) {
   m_props.setProperty("mail.pop3.forgettopheaders","true");
  }
  
  // Get a Session object
  if (mbox.isUseSsl()) {
   m_props.setProperty("mail.pop3.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
   m_props.setProperty("mail.pop3.socketFactory.fallback", "false");
   m_props.setProperty("mail.pop3.port", mbox.getPort()+"");
   m_props.setProperty("mail.pop3.socketFactory.port", mbox.getPort()+"");
   session = Session.getInstance(m_props);
  }
  else {
   session = Session.getInstance(m_props, null);
  }
  
  processor = new MailProcessor(mbox);
 }
 
 /**
  * invoke application plug-in to process e-mails.
  * 
  * @throws MessagingException
  * @throws IOException
  */
 private void readMail() throws MessagingException, IOException {
  session.setDebug(true); // DON'T CHANGE THIS
  String protocol = mailbox.getProtocol();
  if (!"imap".equalsIgnoreCase(protocol) && !"pop3".equalsIgnoreCase(protocol)) {
   throw new IllegalArgumentException("Invalid protocol " + protocol);
  }
  if (store == null) {
   try {
    // Get a Store object
    store = session.getStore(protocol);
    store.addConnectionListener(this);
    store.addStoreListener(this);
   }
   catch (NoSuchProviderException pe) {
    logger.fatal("NoSuchProviderException caught during session.getStore()", pe);
    throw pe;
   }
  }
  try {
   connect(store, 0, mailbox.getMaxRetries()); // could fail due to authentication error
   folder = getFolder(store, 0, 1); // retry once on folder
   // reset debug mode
   session.setDebug(debugSession);
   if ("imap".equalsIgnoreCase(protocol)) {
    // only IMAP support MessageCountListener
    final String _folder = mailbox.getFolderName();
    // Add messageCountListener to listen to new messages from IMAP server
    addMsgCountListener(folder, _folder);
   }
   if ("pop3".equalsIgnoreCase(protocol)) {
    readFromPop3();
   }
   else if ("imap".equalsIgnoreCase(protocol)) {
    readFromImap();
   }
  }
  catch (InterruptedException e) {
   logger.warn("InterruptedException caught, exiting...", e);
  }
  finally {
   try {
    if (folder != null && folder.isOpen()) {
     folder.close(false);
    }
    store.close();
   }
   catch (Exception e) {
    logger.error("Exception caught", e);
   }
  }
  if (isDebugEnabled)
   logger.debug("MailReader ended");
 }

 private void readFromPop3() throws InterruptedException, MessagingException, IOException {
  final String _user = mailbox.getUserId();
  final String _host = mailbox.getHost();
  final String _folder = mailbox.getFolderName();
  boolean keepRunning = true;
  int retries = 0;
  do {
   try {
    if (folder.isOpen()) {
     folder.close(false);
    }
   }
   catch (MessagingException em) {
    logger.error("MessagingException caught during folder.close()", em);
   }
   try {
    Thread.sleep(pollingFreq); // exit if interrupted
    // reopen the folder in order to pick up the new messages
    folder.open(Folder.READ_WRITE);
   }
   catch (MessagingException e) {
    logger.error("Failed to open folder " + _user + "@" + _host + ":" + _folder);
    logger.error("MessagingException caught", e);
    if (retries++ < mailbox.getMaxRetries() || mailbox.getMaxRetries() < 0) {
     int sleepFor;
     // wait for a while and try to reopen the folder
     if (retries < RetryFreqs.length) {
      sleepFor = RetryFreqs[retries];
     }
     else {
      sleepFor = RETRY_FREQ;
     }
     logger.error("Exception caught during folder.open(), retry(=" + retries
       + ") in " + sleepFor + " seconds");
     Thread.sleep(sleepFor * 1000);
      // terminate if interrupted
     continue;
    }
    else {
     logger.fatal("All retries failed for " + _user + "@" + _host + ":" + _folder);
     throw e;
    }
   }
   if (retries > 0) {
    logger.warn("Opened " + _user + "@" + _host + ":" + _folder + " after " + retries + " retries");
    retries = 0; // reset retry counter
   }
   Date start_tms = new Date();
   int msgCount;
   if ((msgCount = folder.getMessageCount()) > 0) {
    logger.info(mailbox.getUserId() + "'s " + _folder + " has " + msgCount + " messages.");
    // "msgsPerRead" is used so the flagged messages will be purged more often
    int msgsToRead = Math.min(msgCount, msgsPerRead);
    // if we can't keep up, process more messages in each cycle
    if (msgCount > msgsToRead * 50) {
     msgsToRead *= 50;
    }
    else if (msgCount > msgsToRead * 10) {
     msgsToRead *= 10;
    }
    else if (msgCount > msgsToRead * 5) {
     msgsToRead *= 5;
    }
    msgsToRead = msgsToRead > MAX_MSGS_PER_READ ? MAX_MSGS_PER_READ : msgsToRead;
    logger.info("number of messages to be read in this cycle: " + msgsToRead);
    Message[] msgs = null;
    try {
     msgs = folder.getMessages(1, msgsToRead);
    }
    catch (IndexOutOfBoundsException ie) {
     logger.error("IndexOutOfBoundsException caught, retry with getMessages()", ie);
     msgs = folder.getMessages();
     logger.info("Retry with folder.getMessages() is successful.");
    }
    execute(msgs); // process the messages read
    folder.close(true); // "true" to delete the flagged messages
    logger.info(msgs.length + " messages have been purged from pop3 mailbox.");
    messagesProcessed += msgs.length;
    long proc_time = new Date().getTime() - start_tms.getTime();
    if (isDebugEnabled)
     logger.debug(msgs.length+ " messages read, time taken: " + proc_time);
   }
  } while (keepRunning); // end of do-while
 }
 
 private void readFromImap() throws MessagingException, InterruptedException, IOException {
  boolean keepRunning = true;
  folder.open(Folder.READ_WRITE);
  /*
   * fix for some IMAP servers: some IMAP servers wouldn't pick up the
   * existing messages, the MessageCountListener may not be implemented
   * correctly for those servers.
   */
  if (folder.getMessageCount() > 0) {
   logger.info(mailbox.getUserId() + "'s " + mailbox.getFolderName() + " has "
     + folder.getMessageCount() + " messages.");
   Date start_tms = new Date();
   Message msgs[] = folder.getMessages();
   execute(msgs);
   folder.expunge(); // remove messages marked as DELETED
   logger.info(msgs.length + " messages have been expunged from imap mailbox.");
   long proc_time = new Date().getTime() - start_tms.getTime();
   if (isDebugEnabled)
    logger.debug(msgs.length+ " messages read, time taken: " + proc_time);
  }
  /* end of the fix */
  while (keepRunning) {
   Thread.sleep(pollingFreq); // sleep for "pollingFreq"
   // This is to force the IMAP server to send us
   // EXISTS notifications.
   folder.getMessageCount();
  }
 }
 
 /**
  * Add messageCountListener to listen to new messages for IMAP.
  * 
  * @param folder -
  *            a Folder object
  * @param _folder -
  *            folder name
  */
 private void addMsgCountListener(final Folder folder, final String _folder) {
  folder.addMessageCountListener(new MessageCountAdapter() {
   private final Logger logger = Logger.getLogger(MessageCountAdapter.class);
   public void messagesAdded(MessageCountEvent ev) {
    Message[] msgs = ev.getMessages();
    logger.info("Got " + msgs.length + " new messages from " + _folder);
    Date start_tms = new Date();
    try {
     execute(msgs);
     folder.expunge(); // remove messages marked as DELETED
     logger.info(msgs.length + " messages have been expunged from imap mailbox.");
     messagesProcessed += msgs.length;
    }
    catch (MessagingException ex) {
     logger.fatal("MessagingException caught", ex);
     throw new RuntimeException(ex.getMessage());
    }
    catch (IOException ex) {
     logger.fatal("IOException caught", ex);
     throw new RuntimeException(ex.getMessage());
    }
    finally {
     long proc_time = new Date().getTime() - start_tms.getTime();
     if (isDebugEnabled)
      logger.debug(msgs.length+ " messages processed, time taken: " + proc_time);
    }
   }
  }); // end of IMAP folder.addMessageCountListener
 }
 
 /*
  * process e-mails.
  * 
  * @param msgs -
  *            messages to be processed.
  * @throws MessagingException
  * @throws IOException
  */
 private void execute(Message[] msgs) throws IOException, MessagingException {
  if (msgs == null || msgs.length == 0) return;
  processor.process(msgs);
 }
 
 /**
  * implement ConnectionListener interface
  * 
  * @param e -
  *            Connection event
  */
 public void opened(ConnectionEvent e) {
  if (isDebugEnabled)
   logger.debug(">>> ConnectionListener: connection opened()");
 }

 /**
  * implement ConnectionListener interface
  * 
  * @param e -
  *            Connection event
  */
 public void disconnected(ConnectionEvent e) {
  logger.info(">>> ConnectionListener: connection disconnected()");
 }

 /**
  * implement ConnectionListener interface
  * 
  * @param e -
  *            Connection event
  */
 public void closed(ConnectionEvent e) {
  if (isDebugEnabled)
   logger.debug(">>> ConnectionListener: connection closed()");
 }

 public void notification(StoreEvent e) {
  if (isDebugEnabled)
   logger.debug(">>> StoreListener: notification event: " + e.getMessage());
 }
 
 /* end of the implementation */

 /**
  * connect to Store with retry logic.
  * 
  * @param store
  *            Store object
  * @param retries
  *            number of retries performed
  * @param maxRetries
  *            number of retries to be performed before giving up
  * @throws MessagingException 
  *             when retries reached the maxRetries
  * @throws InterruptedException 
  */
 void connect(Store store, int retries, int maxRetries) throws MessagingException,
   InterruptedException {
  int portnbr = mailbox.getPort();
  // -1 to use the default port
  if (isDebugEnabled)
   logger.debug("Port used: " + portnbr);
  if (retries > 0) { // retrying, close store first
   try {
    store.close();
   }
   catch (MessagingException e) {
    logger.error("MessagingException caught during retry on store.close()", e);
   }
  }
  try {
   // connect
   store.connect(mailbox.getHost(), portnbr, mailbox.getUserId(), mailbox.getUserPswd());
  }
  catch (MessagingException me) {
   if (retries < maxRetries || maxRetries < 0) {
    int sleepFor;
    if (retries < RetryFreqs.length) {
     sleepFor = RetryFreqs[retries];
    }
    else {
     sleepFor = RETRY_FREQ;
    }
    logger.error("MessagingException caught during store.connect, retry(=" + retries
      + ") in " + sleepFor + " seconds");
    try {
     Thread.sleep(sleepFor * 1000);
    }
    catch (InterruptedException e) {
     logger.warn("InterruptedException caught", e);
     throw e;
    }
    connect(store, ++retries, maxRetries);
   }
   else {
    logger.fatal("Exception caught during store.connect, all retries failed...");
    throw me;
   }
  }
 }

 /**
  * retrieve Folder with retry logic.
  * 
  * @param store
  *            Store object
  * @param retries
  *            number of retries performed
  * @param maxRetries
  *            number of retries to be performed before giving up
  * @return Folder instance
  * @throws MessagingException 
  * @throws InterruptedException 
  */
 Folder getFolder(Store store, int retries, int maxRetries) throws MessagingException,
   InterruptedException {
  try {
   // Open a Folder
   //folder = store.getDefaultFolder();
   folder = store.getFolder(mailbox.getFolderName());

   if (folder == null || !folder.exists()) {
    throw new MessagingException("Invalid folder " + mailbox.getFolderName());
   }
  }
  catch (MessagingException me) {
   if (retries < maxRetries || maxRetries < 0) {
    int sleepFor;
    if (retries < RetryFreqs.length) {
     sleepFor = RetryFreqs[retries];
    }
    else {
     sleepFor = RETRY_FREQ;
    }
    logger.error("MessagingException caught during store.getFolder, retry(=" + retries
      + ") in " + sleepFor + " seconds");
    try {
     Thread.sleep(sleepFor * 1000);
    }
    catch (InterruptedException e) {
     logger.warn("InterruptedException caught", e);
     throw e;
    }
    return getFolder(store, ++retries, maxRetries);
   }
   else {
    logger.fatal("Exception caught during store.getFolder, all retries failed");
    throw me;
   }
  }
  return folder;
 }
}

Build a reliable email reader - Part 2

Here are the two supporting classes needed by the MailReader class. The first one is the Mailbox class that is used to hold all properties of a mailbox. You will need at least three properties to initialize an instance: pop3 server address, mailbox user name, and mailbox password. Use setters to override default values of other properties. Please notice that with Sun provider only the default folder name ("INBOX") is supported.
/*
 * blog/javaclue/javamail/Mailbox.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see .
 */
package blog.javaclue.javamail;

import java.io.Serializable;

public class Mailbox implements Serializable {
 private static final long serialVersionUID = 826439429623556631L;

 private final String userId; 
 private final String userPswd;
 private final String host;

 private int port;
 private String protocol;
 private String folderName;
 private int messagesPerRead;
 private boolean useSsl;
 private int maxRetries;
 private int minimumWait; // in seconds
 private boolean isExchange;
 
 public Mailbox(String host, String userId, String userPswd) {
  this.host = host;
  this.userId = userId;
  this.userPswd = userPswd;
  initDefault();
 }
 
 void initDefault() {
  port = -1; // use protocol default port
  protocol = "pop3";
  folderName = "INBOX";
  messagesPerRead = 10;
  useSsl = false;
  maxRetries = -1;
  minimumWait = 5;
  isExchange = false;
 }

 public String getFolderName() {
  return folderName;
 }
 public void setFolderName(String folderName) {
  this.folderName = folderName;
 }
 public String getHost() {
  return host;
 }
 public int getMinimumWait() {
  return minimumWait;
 }
 public void setMinimumWait(int minimumWait) {
  this.minimumWait = minimumWait;
 }
 public int getPort() {
  return port;
 }
 public void setPort(int port) {
  this.port = port;
 }
 public String getProtocol() {
  return protocol;
 }
 public void setProtocol(String protocol) {
  this.protocol = protocol;
 }
 public int getMessagesPerRead() {
  return messagesPerRead;
 }
 public void setMessagesPerRead(int readPerPass) {
  this.messagesPerRead = readPerPass;
 }
 public int getMaxRetries() {
  return maxRetries;
 }
 public void setMaxRetries(int retryMax) {
  this.maxRetries = retryMax;
 }
 public String getUserId() {
  return userId;
 }
 public String getUserPswd() {
  return userPswd;
 }
 public boolean isUseSsl() {
  return useSsl;
 }
 public void setUseSsl(boolean useSsl) {
  this.useSsl = useSsl;
 }

 public boolean isExchange() {
  return isExchange;
 }

 public void setExchange(boolean isExchange) {
  this.isExchange = isExchange;
 }
}
The next class is the MailProcessor class which is used to process the email messages read by the MailReader. This is most likely the class you would customize when building your own back-end mail reader.
/*
 * blog/javaclue/javamail/MailProcessor.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package blog.javaclue.javamail;

import java.io.IOException;
import java.util.Date;

import javax.mail.Flags;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Part;

import org.apache.log4j.Logger;

/**
 * process email's handed over by MailReader class.
 * 
 * @author JackW
 */
public class MailProcessor {
 static final Logger logger = Logger.getLogger(MailProcessor.class);
 static final boolean isDebugEnabled = logger.isDebugEnabled();

 protected final String LF = System.getProperty("line.separator", "\n");
 private final Mailbox mailbox;
 
 private static final int MAX_BODY_SIZE = 150 * 1024; // 150KB
 private static final int MAX_CMPT_SIZE = 1024 * 1024; // 1MB
 private static final int MAX_TOTAL_SIZE = 10 * 1024 * 1024; // 10MB

 public MailProcessor(Mailbox mailbox) {
  this.mailbox = mailbox;
 }

 /**
  * process messages.
  * 
  * @param msgs -
  *            array of Messages.
  * @throws MessagingException
  * @throws IOException 
  */
 public void process(Message[] msgs) throws MessagingException, IOException {
  if (isDebugEnabled)
   logger.debug("Entering process() method...");
  for (int i = 0; i < msgs.length; i++) {
   if (msgs[i] != null && !msgs[i].isSet(Flags.Flag.SEEN)
     && !msgs[i].isSet(Flags.Flag.DELETED)) {
    processPart(msgs[i]);
    // message has been processed, delete it from mail box
    msgs[i].setFlag(Flags.Flag.DELETED, true);
   }
  }
 }

 /**
  * process message part
  * 
  * @param p -
  *            part
  * @throws MessagingException 
  * @throws IOException 
  */
 MessageBean processPart(Part p) throws IOException, MessagingException {
  Date start_tms = new Date();
  
  // parse the MimeMessage to MessageBean
  MessageBean msgBean = MessageBeanUtil.mimeToBean(p);
  
  // MailBox Host Address
  msgBean.setMailboxHost(mailbox.getHost());
  // MailBox User Id
  msgBean.setMailboxUser(mailbox.getUserId());
  
  // get message body
  String body = msgBean.getBody();

  // check message body and component size
  boolean msgSizeTooLarge = false;
  if (body.length() > MAX_BODY_SIZE) {
   msgSizeTooLarge = true;
   logger.warn("Message body size exceeded limit: " + body.length());
  }
  int totalSize = body.length();
  if (!msgSizeTooLarge && msgBean.getComponentsSize().size() > 0) {
   for (int i = 0; i < msgBean.getComponentsSize().size(); i++) {
    Integer objSize = (Integer) msgBean.getComponentsSize().get(i);
    if (objSize.intValue() > MAX_CMPT_SIZE) {
     msgSizeTooLarge = true;
     logger.warn("Message component(" + i + ") exceeded limit: " + objSize.intValue());
     break;
    }
    totalSize += objSize;
   }
  }
  if (!msgSizeTooLarge && totalSize > MAX_TOTAL_SIZE) {
   logger.warn("Message total size exceeded limit: " + totalSize);
   msgSizeTooLarge = true;
  }
  
  if (msgSizeTooLarge) {
   logger.error("The email message has been rejected due to its size");
   // XXX - add your code here to deal with it
  }
  else { // email size within the limit
   if (msgBean.getSmtpMessageId() == null) {
    logger.warn("SMTP Message-Id is null, FROM Address = " + msgBean.getFromAsString());
   }
   if (isDebugEnabled)
    logger.debug("Message read..." + LF + msgBean);
   // XXX: Add you code here to process the message ...
  }
  if (isDebugEnabled && msgBean.getAttachCount() > 0)
   logger.debug("Number of attachments receibved: " + msgBean.getAttachCount());

  long time_spent = new Date().getTime() - start_tms.getTime();
  if (isDebugEnabled)
   logger.debug("Msg from " + msgBean.getFromAsString() + " processed, " + time_spent);
  
  return msgBean;
 }
}

Friday, September 4, 2009

Simple IBM MQ Queue Depth Monitor

For those that are still running IBM MQ (or Websphere MQ) on mainframes, presented here is a simple java queue depth monitor program that polls a queue manager periodically for the number of messages in a queue. The program utilizes the classes provided in the PCF package to get the queue depth information. This sample code will output a message to the console when the queue contains more than 10 messages at the moment of polling. You can change it to send out an email or snmp alert instead.
In order to compile and run this program, PCF package (MS0B) is needed and can be downloaded from IBM. You will also need "com.ibm.mq.jar" and IBM's "j2ee.jar". They can be found from IBM's WSAD or RAD IDE development tools.
/*
 * blog/javaclue/ibmmq/QueueDepthMonitor.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package blog.javaclue.ibmmq;

import java.io.IOException;

import org.apache.log4j.Logger;

import com.ibm.mq.MQException;
import com.ibm.mq.pcf.CMQC;
import com.ibm.mq.pcf.CMQCFC;
import com.ibm.mq.pcf.PCFException;
import com.ibm.mq.pcf.PCFMessage;
import com.ibm.mq.pcf.PCFMessageAgent;

/**
 * Simple queue depth monitor program that uses PCFAgent to generate and parse
 * a PCF query.
 */
public class QueueDepthMonitor implements Runnable {
 protected static Logger logger = Logger.getLogger(QueueDepthMonitor.class);
 protected static boolean isDebugEnabled = logger.isDebugEnabled();

 final String qmgrName;
 final String host;
 final int port;
 final String channel;
 final String queueName;
 final int alertDepth;

 final static int Polling_Freq = 30 * 1000; // 30 seconds

 QueueDepthMonitor(String name, String host, String port, String channel, String queueName,
   int alertDepth) {
  this.qmgrName = name;
  this.host = host;
  this.channel = channel;
  this.port = Integer.parseInt(port);
  this.queueName = queueName;
  this.alertDepth = alertDepth;
 }

 public void run() {
  if (isDebugEnabled)
   logger.debug("Starting Queue Depth monitor for " + queueName + "...");
  while (true) {
   checkDepth();
   try {
    Thread.sleep(Polling_Freq); // sleep for 30 seconds
   }
   catch (InterruptedException e) {
    logger.info("The monitor has been interrupted, exit...");
    break;
   }
  }
 }

 private void checkDepth() {
  PCFMessageAgent agent = null;
  int[] attrs = { CMQC.MQCA_Q_NAME, CMQC.MQIA_CURRENT_Q_DEPTH };
  PCFMessage request = new PCFMessage(CMQCFC.MQCMD_INQUIRE_Q);
  request.addParameter(CMQC.MQCA_Q_NAME, queueName);
  request.addParameter(CMQC.MQIA_Q_TYPE, CMQC.MQQT_LOCAL);
  request.addParameter(CMQCFC.MQIACF_Q_ATTRS, attrs);
  PCFMessage[] responses;

  if (isDebugEnabled) {
   logger.debug("Connecting to " + qmgrName + " at " + host + ":" + port + " over " + channel);
  }
  try {
   // Connect a PCFAgent to the queue manager
   agent = new PCFMessageAgent(host, port, channel);
   // Use the agent to send the request
   responses = agent.send(request);
   // retrieving queue depth
   for (int i = 0; i < responses.length; i++) {
    String name = responses[i].getStringParameterValue(CMQC.MQCA_Q_NAME);
    int depth = responses[i].getIntParameterValue(CMQC.MQIA_CURRENT_Q_DEPTH);
    if (isDebugEnabled && name != null)
     logger.debug("Queue " + name + " Depth " + depth);
    if (name != null && queueName.equals(name.trim())) { // just for safety
     if (depth > alertDepth) {
      logger.info(qmgrName + "/" + queueName + " depth = " + depth
        + ", exceeded alert threshold: " + alertDepth);
      // XXX: add your code here to send out alert
     }
    }
   }
  }
  catch (PCFException pcfe) {
   logger.error("PCFException caught", pcfe);
   PCFMessage[] msgs = (PCFMessage[]) pcfe.exceptionSource;
   for (int i = 0; i < msgs.length; i++) {
    logger.error(msgs[i]);
   }
  }
  catch (MQException mqe) {
   logger.error("MQException caught", mqe);
  }
  catch (IOException ioe) {
   logger.error("IOException caught", ioe);
  }
  finally {
   // Disconnect
   if (agent != null) {
    try {
     agent.disconnect();
    }
    catch (Exception e) {
     logger.error("Exception caught during disconnect", e);
    }
   }
   else {
    logger.warn("unable to disconnect, agent is null.");
   }
  }
 }
 
 public static void main(String[] args) {
  String qmgrName = "QMGR";
  String host = "localhost";
  String port = "1450";
  String channel = "SYSTEM.DEF.SVRCONN";
  String queueName = "TEST_QUEUE";

  QueueDepthMonitor monitor = new QueueDepthMonitor(qmgrName, host, port, channel, queueName, 10);
  new Thread(monitor).start();
 }
}

Detect bounced emails - Part 3

Now it's the time for the bounce finder class. A simple method called "parse" is provided in the class that takes a message bean as input and returns a bounce type. This method will always return a value even when the email is not a rejected message, the word "GENERIC" is returned in this case.

/*
 * blog/javaclue/javamail/BounceFinder.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package blog.javaclue.javamail;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;

import javax.mail.Address;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;

import org.apache.log4j.Logger;

import blog.javaclue.javamail.SmtpScanner.BOUNCE_TYPES;

/**
 * Scan email header and body, and match rules to determine the bounce type.
 * 
 * @author jackw
 */
public final class BounceFinder {
 static final Logger logger = Logger.getLogger(BounceFinder.class);
 static final boolean isDebugEnabled = logger.isDebugEnabled();

 private final SmtpScanner rfcScan;

 static final String TEN_DASHES = "----------";
 static final String ORIGMSG_SEPARATOR = "-----Original Message-----";
 static final String REPLY_SEPARATOR = "---------Reply Separator---------";
 static final String LF = System.getProperty("line.separator", "\n");

 public final static String VERP_BOUNCE_ADDR_XHEADER = "X-VERP_Bounce_Addr";

 /**
  * default constructor
  */
 public BounceFinder() throws IOException {
  rfcScan = SmtpScanner.getInstance();
 }

 /**
  * Scans email properties to find out the bounce type. It also checks VERP
  * headers to get original recipient.
  * 
  * @param msgBean
  *            a MessageBean instance
  */
 public String parse(MessageBean msgBean) {
  if (isDebugEnabled)
   logger.debug("Entering parse() method...");
  String bounceType = null;
  
  // retrieve attachments into an array, it also gathers rfc822/Delivery Status.
  BodypartUtil.retrieveAttachments(msgBean);

  // scan message for Enhanced Mail System Status Code (rfc1893/rfc3464)
  BodypartBean aNode = null;
  if (msgBean.getReport() != null) {
   /*
    * multipart/report mime type is present, retrieve DSN/MDN report.
    */
   MessageNode mNode = msgBean.getReport();
   // locate message/delivery-status section
   aNode = BodypartUtil.retrieveDlvrStatus(mNode.getBodypartNode(), mNode.getLevel());
   if (aNode != null) {
    // first scan message/delivery-status
    byte[] attchValue = (byte[]) aNode.getValue();
    if (attchValue != null) {
     if (isDebugEnabled) {
      logger.debug("parse() - scan message/report status -----<" + LF + new String(attchValue) + ">-----");
     }
     if (bounceType == null) {
      bounceType = rfcScan.scanBody(new String(attchValue));
     }
     parseDsn(attchValue, msgBean);
     msgBean.setDsnDlvrStat(new String(attchValue));
    }
   }
   else if ((aNode = BodypartUtil.retrieveMDNReceipt(mNode.getBodypartNode(), mNode.getLevel())) != null) {
    // got message/disposition-notification
    byte[] attchValue = (byte[]) aNode.getValue();
    if (attchValue != null) {
     if (isDebugEnabled) {
      logger.debug("parse() - display message/report status -----<" + LF + new String(attchValue) + ">-----");
     }
     if (bounceType == null) {
      bounceType = BOUNCE_TYPES.MDN_RECEIPT.toString();
     }
     // MDN comes with original and final recipients
     parseDsn(attchValue, msgBean);
     msgBean.setDsnDlvrStat(new String(attchValue));
    }
   }
   else {
    // missing message/* section, try text/plain
    List<BodypartBean> nodes = BodypartUtil.retrieveReportText(mNode.getBodypartNode(), mNode.getLevel());
    if (!nodes.isEmpty()) {
     ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
     for (BodypartBean bodyPart : nodes) {
      byte[] attchValue = (byte[]) bodyPart.getValue();
      try {
       baos.write(attchValue);
      }
      catch (IOException e) {
       logger.error("IOException caught", e);
      }
     }
     try {
      baos.close();
     }
     catch (IOException e) {}
     byte[] attchValue = baos.toByteArray();
     if (attchValue != null) {
      if (isDebugEnabled) {
       logger.debug("parse() - scan message/report text -----<" + LF + new String(attchValue) + ">-----");
      }
      if (bounceType == null) {
       bounceType = rfcScan.scanBody(new String(attchValue));
      }
      parseDsn(attchValue, msgBean);
      msgBean.setDsnText(new String(attchValue));
     }
    }
   }
   // locate possible message/rfc822 section under multipart/report
   aNode = BodypartUtil.retrieveMessageRfc822(mNode.getBodypartNode(), mNode.getLevel());
   if (aNode != null && msgBean.getRfc822() == null) {
    msgBean.setRfc822(new MessageNode(aNode, mNode.getLevel()));
   }
   // locate possible text/rfc822-headers section under multipart/report
   aNode = BodypartUtil.retrieveRfc822Headers(mNode.getBodypartNode(), mNode.getLevel());
   if (aNode != null && msgBean.getRfc822() == null) {
    msgBean.setRfc822(new MessageNode(aNode, mNode.getLevel()));
   }
  }

  if (msgBean.getRfc822() != null) {
   /*
    * message/rfc822 is present, retrieve RFC report.
    */
   MessageNode mNode = msgBean.getRfc822();
   aNode = BodypartUtil.retrieveRfc822Text(mNode.getBodypartNode(), mNode.getLevel());
   if (aNode != null) {
    StringBuffer sb = new StringBuffer();
    // get original message headers
    List<MsgHeader> vheader = aNode.getHeaders();
    for (int i = 0; vheader != null && i < vheader.size(); i++) {
     MsgHeader header = vheader.get(i);
     sb.append(header.getName() + ": " + header.getValue() + LF);
    }
    boolean foundAll = false;
    String rfcHeaders = sb.toString();
    if (!StringUtil.isEmpty(rfcHeaders)) {
     // rfc822 headers
     if (isDebugEnabled) {
      logger.debug("parse() - scan rfc822 headers -----<" + LF + rfcHeaders + ">-----");
     }
     foundAll = parseRfc(rfcHeaders, msgBean);
     msgBean.setDsnRfc822(rfcHeaders);
    }
    byte[] attchValue = (byte[]) aNode.getValue();
    if (attchValue != null) {
     // rfc822 text
     String rfcText = new String(attchValue);
     sb.append(rfcText);
     String mtype = aNode.getMimeType();
     if (mtype.startsWith("text/") || mtype.startsWith("message/")) {
      if (foundAll == false) {
       if (isDebugEnabled) {
        logger.debug("parse() - scan rfc822 text -----<" + LF + rfcText + ">-----");
       }
       parseRfc(rfcText, msgBean);
       msgBean.setDsnRfc822(sb.toString());
      }
     }
     if (msgBean.getDsnText() == null) {
      msgBean.setDsnText(rfcText);
     }
     else {
      msgBean.setDsnText(msgBean.getDsnText() + LF + LF + "RFC822 Text:" + LF + rfcText);
     }
    }
    if (bounceType == null) {
     bounceType = rfcScan.scanBody(sb.toString());
    }
   }
  } // end of RFC Scan

  String body = msgBean.getBody();
  if (msgBean.getRfc822() != null && bounceType == null) {
   // message/rfc822 is present, scan message body for rfc1893 status code
   // TODO: may cause false positives. need to revisit this.
   if (isDebugEnabled)
    logger.debug("parse() - scan body text -----<" + LF + body + ">-----");
   bounceType = rfcScan.scanBody(body);
  }

  // check CC/BCC
  if (bounceType == null) {
   // if the "real_to" address is not found in envelope, but is
   // included in CC or BCC: set bounceType to CC_USER
   for (int i = 0; msgBean.getTo() != null && i < msgBean.getTo().length; i++) {
    Address to = msgBean.getTo()[i];
    if (containsNoAddress(msgBean.getToEnvelope(), to)) {
     if (containsAddress(msgBean.getCc(), to)
       || containsAddress(msgBean.getBcc(), to)) {
      bounceType = BOUNCE_TYPES.CC_USER.toString();
      break;
     }
    }
   }
  }

  // check VERP bounce address, set bounce type to SOFT_BOUNCE if VERP recipient found
  List<MsgHeader> headers = msgBean.getHeaders();
  for (MsgHeader header : headers) {
   if (VERP_BOUNCE_ADDR_XHEADER.equals(header.getName())) {
    logger.info("parse() - VERP Recipient found: ==>" + header.getValue() + "<==");
    if (msgBean.getOrigRcpt() != null && !StringUtil.isEmpty(header.getValue())
      && !msgBean.getOrigRcpt().equalsIgnoreCase(header.getValue())) {
     logger.warn("parse() - replace original recipient: " + msgBean.getOrigRcpt()
       + " with VERP recipient: " + header.getValue());
    }
    if (!StringUtil.isEmpty(header.getValue())) {
     // VERP Bounce - always override
     msgBean.setOrigRcpt(header.getValue());
    }
    else {
     logger.warn("parse() - " + VERP_BOUNCE_ADDR_XHEADER + " Header found, but it has no value.");
    }
    if (bounceType == null) {
     // a bounced mail shouldn't have Return-Path
     String rPath = msgBean.getReturnPath() == null ? "" : msgBean.getReturnPath();
     if (StringUtil.isEmpty(rPath) || "<>".equals(rPath.trim())) {
      bounceType = BOUNCE_TYPES.SOFT_BOUNCE.toString();
     }
    }
    break;
   }
  }

  // if it's hard or soft bounce and no final recipient was found, scan
  // message body for final recipient using known patterns.
  if (BOUNCE_TYPES.HARD_BOUNCE.toString().equals(bounceType)
    || BOUNCE_TYPES.SOFT_BOUNCE.toString().equals(bounceType)) {
   if (StringUtil.isEmpty(msgBean.getFinalRcpt())
     && StringUtil.isEmpty(msgBean.getOrigRcpt())) {
    String finalRcpt = BounceAddressFinder.getInstance().find(body);
    if (!StringUtil.isEmpty(finalRcpt)) {
     logger.info("parse() - Final Recipient found from message body: " + finalRcpt);
     msgBean.setFinalRcpt(finalRcpt);
    }
   }
  }

  if (bounceType == null) { // use default
   bounceType = SmtpScanner.BOUNCETYPE.GENERIC.toString();
  }

  logger.info("parse() - bounceType: " + bounceType);

  return bounceType;
 }

 private boolean containsAddress(Address[] addrs, Address to) {
  if (to != null && addrs != null && addrs.length > 0) {
   for (int i = 0; i < addrs.length; i++) {
    if (to.equals(addrs[i])) {
     return true;
    }
   }
  }
  return false;
 }

 private boolean containsNoAddress(Address[] addrs, Address to) {
  if (to != null && addrs != null && addrs.length > 0) {
   for (int i = 0; i < addrs.length; i++) {
    if (to.equals(addrs[i])) {
     return false;
    }
   }
  }
  return true;
 }

 /**
  * Parse the message/delivery-status to retrieve DSN fields. Also used by
  * message/disposition-notification to retrieve final recipient.
  * 
  * @param attchValue -
  *            delivery status text
  * @param msgBean -
  *            MessageBean object
  */
 private void parseDsn(byte[] attchValue, MessageBean msgBean) {
  // retrieve Final-Recipient, Action, and Status
  ByteArrayInputStream bais = new ByteArrayInputStream(attchValue);
  BufferedReader br = new BufferedReader(new InputStreamReader(bais));
  String line = null;
  try {
   while ((line = br.readLine()) != null) {
    if (isDebugEnabled)
     logger.debug("parseDsn() - Line: " + line);
    line = line.trim();
    if (line.toLowerCase().startsWith("final-recipient:")) {
     // "Final-Recipient" ":" address-type ";" generic-address
     // address-type = rfc822 / unknown
     StringTokenizer st = new StringTokenizer(line, " ;");
     while (st.hasMoreTokens()) {
      String token = st.nextToken().trim();
      if (token.indexOf("@") > 0) {
       msgBean.setFinalRcpt(token);
       logger.info("parseDsn() - Final_Recipient found: ==>" + token + "<==");
       break;
      }
     }
    }
    else if (line.toLowerCase().startsWith("original-recipient:")) {
     // "Original-Recipient" ":" address-type ";" generic-address
     StringTokenizer st = new StringTokenizer(line, " ;");
     while (st.hasMoreTokens()) {
      String token = st.nextToken().trim();
      if (token.indexOf("@") > 0) {
       msgBean.setOrigRcpt(token);
       logger.info("parseDsn() - Original_Recipient found: ==>" + token + "<==");
       break;
      }
     }
    }
    else if (line.toLowerCase().startsWith("action:")) {
     /**
      * "Action" ":" action-value = 
      * 1) failed - could not be delivered to the recipient.
      * 2) delayed - the reporting MTA has so far been unable to deliver
      *  or relay the message.
      * 3) delivered - the message was successfully delivered.
      * 4) relayed - the message has been relayed or gatewayed.
      * 5) expanded - delivered and forwarded by reporting MTA to multiple
      *  additional recipient addresses.
      */ 
     String action = line.substring(7).trim();
     msgBean.setDsnAction(action);
     if (isDebugEnabled)
      logger.debug("parseDsn() - Action found: ==>" + action + "<==");
    }
    else if (line.toLowerCase().startsWith("status:")) {
     // "Status" ":" status-code (digit "." 1*3digit "." 1*3 digit)
     String status = line.substring(7).trim();
     if (status.indexOf(" ") > 0) {
      status = status.substring(0, status.indexOf(" "));
     }
     msgBean.setDsnStatus(status);
     if (isDebugEnabled)
      logger.debug("parseDsn() - Status found: ==>" + status + "<==");
    }
    else if (line.toLowerCase().startsWith("diagnostic-code:")) {
     // "Diagnostic-Code" ":" diagnostic-code
     String diagcode = line.substring(16).trim();
     msgBean.setDiagnosticCode(diagcode);
     if (isDebugEnabled)
      logger.debug("parseDsn() - Diagnostic-Code: found: ==>" + diagcode + "<==");
    }
   }
  }
  catch (IOException e) {
   logger.error("IOException caught during parseDsn()", e);
  }
 }

 /**
  * parse message/rfc822 to retrieve original email properties: final
  * recipient, original subject and original SMTP message-id.
  * 
  * @param rfc_text -
  *            rfc822 text
  * @param msgBean -
  *            MessageBean object
  * @return true if all three properties were found
  */
 private boolean parseRfc(String rfc_text, MessageBean msgBean) {
  // retrieve original To address
  ByteArrayInputStream bais = new ByteArrayInputStream(rfc_text.getBytes());
  BufferedReader br = new BufferedReader(new InputStreamReader(bais));
  int lineCount = 0;
  boolean gotToAddr = false, gotSubj = false, gotSmtpId = false;
   // allows to quit scan once all three headers are found
  String line = null;
  try {
   while ((line = br.readLine()) != null) {
    if (isDebugEnabled)
     logger.debug("parseRfc() - Line: " + line);
    line = line.trim();
    if (line.toLowerCase().startsWith("to:")) {
     // "To" ":" generic-address
     String token = line.substring(3).trim();
     if (StringUtil.isEmpty(msgBean.getFinalRcpt())) {
      msgBean.setFinalRcpt(token);
     }
     else if (StringUtil.compareEmailAddrs(msgBean.getFinalRcpt(), token) != 0) {
      logger.error("parseRfc() - Final_Rcpt from RFC822: " + token + " is different from DSN's: " + msgBean.getFinalRcpt());
     }
     logger.info("parseRfc() - Final_Recipient(RFC822 To) found: ==>" + token + "<==");
     gotToAddr = true;
    }
    else if (line.toLowerCase().startsWith("subject:")) {
     // "Subject" ":" subject text
     String token = line.substring(8).trim();
     if (StringUtil.isEmpty(msgBean.getOrigSubject())) {
      msgBean.setOrigSubject(token);
     }
     logger.info("parseRfc() - Original_Subject(RFC822 To) found: ==>" + token + "<==");
     gotSubj = true;
    }
    else if (line.toLowerCase().startsWith("message-id:")) {
     // "Message-Id" ":" SMTP message id
     String token = line.substring(11).trim();
     if (StringUtil.isEmpty(msgBean.getSmtpMessageId())) {
      msgBean.setRfcMessageId(token);
     }
     logger.info("parseRfc() - Smtp Message-Id(RFC822 To) found: ==>" + token + "<==");
     gotSmtpId = true;
    }
    if (gotToAddr && gotSubj && gotSmtpId) {
     return true;
    }
    if (++lineCount > 100 && line.indexOf(":") < 0) {
     break; // check if it's a header after 100 lines
    }
   } // end of while
  }
  catch (IOException e) {
   logger.error("IOException caught during parseRfc()", e);
  }
  return false;
 }

 public static void main(String[] args) {
  try {
   BounceFinder parser = new BounceFinder();
   MessageBean mBean = new MessageBean();
   try {
    mBean.setFrom(InternetAddress.parse("event.alert@localhost", false));
    mBean.setTo(InternetAddress.parse("abc@domain.com", false));
   }
   catch (AddressException e) {
    logger.error("AddressException caught", e);
   }
   mBean.setSubject("A Exception occured");
   mBean.setValue(new Date()+ " 5.2.2 Invalid user account.");
   mBean.setMailboxUser("testUser");
   String bType = parser.parse(mBean);
   System.out.println("### Bounce Type: " + bType);
  }
  catch (Exception e) {
   e.printStackTrace();
  }
 }
}

After an email is found to be a rejected message, the bounce finder class will also try to find the email address of the intended recipient, as this could be very important in case you want to remove the address from your mailing list. You can get the address by calling getOrigRcpt() or getFinalRcpt() of MessageBean class.
Since there are still many popular mail servers that are not strictly follow the RFC standards, a bounce address finder class is provided to locate the original recipient from message body, in case the bounce finder class failed to find it from RFC components. The patterns are not exact science, rather they are derived from limited samples of rejected emails. Take that into account when you adapt this bounce address finder.

/*
 * blog/javaclue/javamail/BounceAddressFinder.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package blog.javaclue.javamail;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

public final class BounceAddressFinder {
 static final Logger logger = Logger.getLogger(BounceAddressFinder.class);
 static final boolean isDebugEnabled = false; //logger.isDebugEnabled();

 private final List<MyPattern> patternList = new ArrayList<MyPattern>();
 private static BounceAddressFinder addressFinder = null;
 
 private BounceAddressFinder() {
  if (patternList.isEmpty()) {
   loadPatterns();
  }
 }
 
 public static synchronized BounceAddressFinder getInstance() {
  if (addressFinder == null) {
   addressFinder = new BounceAddressFinder();
  }
  return addressFinder;
 }
 
 public String find(String body) {
  if (body != null && body.trim().length() > 0) {
   for (MyPattern myPattern : patternList) {
    Matcher m = myPattern.getPattern().matcher(body);
    if (m.find()) {
     if (isDebugEnabled) {
      for (int i = 1; i <= m.groupCount(); i++) {
       logger.debug(myPattern.getPatternName() + ", group(" + i + ") - " + m.group(i));
      }
     }
     return m.group(m.groupCount());
    }
   }
  }
  return null;
 }
 
 private static final class MyPattern {
  private final String patternName;
  private final String patternRegex;
  private final Pattern pattern;
  MyPattern(String name, String value) {
   this.patternName = name;
   this.patternRegex = value;
   pattern = Pattern.compile(patternRegex, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
  }
  
  public Pattern getPattern() {
   return pattern;
  }
  public String getPatternName() {
   return patternName;
  }
  public String getPatternRegex() {
   return patternRegex;
  }
 }
 
 private final void loadPatterns() {
  String bodyGmail = 
   "Delivery .{4,10} following recipient(?:s)? failed[\\.|\\s](?:permanently:)?\\s+" +
   "<?(" + StringUtil.getEmailRegex() + ")>?\\s+";
  patternList.add(new MyPattern("Gmail",bodyGmail));
  
  String bodyAol = 
   "\\-{3,6} The following address(?:es|\\(es\\))? had (?:permanent fatal errors|delivery problems) \\-{3,6}\\s+" +
   "<?(" + StringUtil.getEmailRegex() + ")>?(?:\\s|;)";
  patternList.add(new MyPattern("AOL",bodyAol));
  
  String bodyYahoo = 
   "This .{1,10} permanent error.\\s+I(?:'ve| have) given up\\. Sorry it did(?:n't| not) work out\\.\\s+" +
   "<?(" + StringUtil.getEmailRegex() + ")>?";
  patternList.add(new MyPattern("Yahoo",bodyYahoo));
  
  String bodyPostfix = 
   "message\\s.*could\\s+not\\s+be\\s+.{0,10}delivered\\s+to\\s.*(?:recipient(?:s)?|destination(?:s)?)" +
   ".{80,180}\\sinclude\\s+this\\s+problem\\s+report.{60,120}" +
   "\\s+<(" + StringUtil.getEmailRegex() + ")>";
  patternList.add(new MyPattern("Postfix",bodyPostfix));
  
  String bodyFailed = 
   "Failed\\s+to\\s+deliver\\s+to\\s+\\'(" + StringUtil.getEmailRegex() + ")\\'" +
   ".{1,20}\\smodule.{5,100}\\sreports";
  patternList.add(new MyPattern("Failed",bodyFailed));
  
  String bodyFirewall = 
   "Your\\s+message\\s+to:\\s+(" + StringUtil.getEmailRegex() + ")\\s+" +
   ".{1,10}\\sblocked\\s+by\\s.{1,20}\\sSpam\\s+Firewall";
  patternList.add(new MyPattern("SpamFirewall",bodyFirewall));
  
  String bodyFailure = 
   "message\\s.{8,20}\\scould\\s+not\\s+be\\s+delivered\\s.{10,40}\\srecipients" +
   ".{6,20}\\spermanent\\s+error.{10,20}\\saddress(?:\\(es\\))?\\s+failed:" +
   "\\s+(" + StringUtil.getEmailRegex() + ")\\s";
  patternList.add(new MyPattern("Failure",bodyFailure));
  
  String bodyUnable = 
   "Unable to deliver message to the following address(?:\\(es\\))?.{0,5}" +
   "\\s+<(" + StringUtil.getEmailRegex() + ")>";
  patternList.add(new MyPattern("Unable",bodyUnable));
  
  String bodyEtrust = 
   "\\scould not deliver the e(?:\\-)?mail below because\\s.{10,20}\\srecipient(?:s)?\\s.{1,10}\\srejected"+
   ".{60,200}\\s(" + StringUtil.getEmailRegex() + ")";
  patternList.add(new MyPattern("eTrust",bodyEtrust));
  
  String bodyReport = 
   "\\scollection of report(?:s)? about email delivery\\s.+\\sFAILED:\\s.{1,1000}" +
   "Final Recipient:.{0,20};\\s*(" + StringUtil.getEmailRegex() + ")";
  patternList.add(new MyPattern("Report",bodyReport));
  
  String bodyNotReach = 
   "Your message.{1,400}did not reach the following recipient(?:\\(s\\))?:" +
   "\\s+(" + StringUtil.getEmailRegex() + ")";
  patternList.add(new MyPattern("NotReach",bodyNotReach));
  
  String bodyFailed2 = 
   "Could not deliver message to the following recipient(?:\\(s\\))?:" +
   "\\s+Failed Recipient:\\s+(" + StringUtil.getEmailRegex() + ")\\s";
  patternList.add(new MyPattern("Failed2",bodyFailed2));
  
  String bodyExceeds = 
   "User(?:'s)?\\s+mailbox\\s+exceeds\\s+allowed\\s+size:\\s+" +
   "(" + StringUtil.getEmailRegex() + ")\\s+";
  patternList.add(new MyPattern("Exceeds",bodyExceeds));
  
  String bodyDelayed = 
   "Message\\s+delivery\\s+to\\s+\\'(" + StringUtil.getEmailRegex() + ")\\'" +
   "\\s+delayed.{1,20}\\smodule.{5,100}\\sreports";
  patternList.add(new MyPattern("Delayed",bodyDelayed));
  
  String bodyInvalid = 
   "Invalid\\s+Address(?:es)?.{1,20}\\b(?:TO|addr)\\b.{1,20}\\s+<?(" + StringUtil.getEmailRegex() + ")>?\\s+";
  patternList.add(new MyPattern("Invalid",bodyInvalid));
 }
}

Detect bounced emails - Part 2

In Part 1 we provided a class to retrieve RFC related components from a mime message. Now we are providing a class that scans the components to find out the reason why the email was rejected.
The reasons are grouped into bounce types. Following are the bounce types recognized by this scanner:
        HARD_BOUNCE
        SOFT_BOUNCE
        MAILBOX_FULL
        CC_USER
        MDN_RECEIPT // read receipt

/*
 * blog/javaclue/javamail/SmtpScanner.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package blog.javaclue.javamail;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.log4j.Logger;

/**
 * Scan input string for RFC1893/RFC2821 mail status code
 * 
 * @author jackw
 */
final class SmtpScanner {
 static final Logger logger = Logger.getLogger(SmtpScanner.class);
 static final boolean isDebugEnabled = logger.isDebugEnabled();
 final int maxLenToScan = 8192*4; // scan up to 32k
 
 private static final HashMap<String, String> RFC1893_STATUS_CODE = new HashMap<String, String>();
 private static final HashMap<String, String> RFC1893_STATUS_DESC = new HashMap<String, String>();
 private static final HashMap<String, String> RFC2821_STATUS_CODE = new HashMap<String, String>();
 private static final HashMap<String, String> RFC2821_STATUS_DESC = new HashMap<String, String>();
 private static final HashMap<String, String> RFC2821_STATUS_MATCHINGTEXT = new HashMap<String, String>();
 
 public static enum BOUNCETYPE { GENERIC }; // default bounce type - not a bounce.
 public static enum BOUNCE_TYPES {
  HARD_BOUNCE, // Hard bounce - suspend,notify,close
  SOFT_BOUNCE, // Soft bounce - bounce++,close
  MAILBOX_FULL, // mailbox full, can be treated as Soft Bounce
  CC_USER, // Mail received as a Carbon Copy
  MDN_RECEIPT, // MDN - read receipt
 }

 private static SmtpScanner smtpCodeScan = null;

 private static final String
  LETTER_S = "s",
  LETTER_H = "h",
  LETTER_F = "f",
  LETTER_K = "k",
  LETTER_U = "u";

 /**
  * default constructor
  */
 private SmtpScanner() throws IOException {
  loadRfc1893StatusCode();
  loadRfc2821StatusCode();
 }
 
 public static SmtpScanner getInstance() throws IOException {
  if (smtpCodeScan==null) {
   smtpCodeScan = new SmtpScanner();
  }
  return smtpCodeScan;
 }
 
 /**
  * returns a message id, null if not found
  * 
  * @param str -
  *            message body
  * @return message id, null if not found
  */
 String scanBody(String body) {
  String bounceType = scanBody(body, 1);
  if (bounceType == null) {
   bounceType = scanBody(body, 2);
  }
  return bounceType;
 }
 
 private static Pattern pattern1 = Pattern.compile("\\s([245]\\.\\d{1,3}\\.\\d{1,3})\\s", Pattern.DOTALL);
 private static Pattern pattern2 = Pattern.compile("\\s([245]\\d\\d)\\s", Pattern.DOTALL);
 
 /**
  * <ul>
  * <li> first pass: check if it contains a RFC1893 code. RFC1893 codes are
  * from 5 to 9 bytes long (x.x.x -> x.xxx.xxx) and start with 2.x.x or 4.x.x
  * or 5.x.x
  * <li> second pass: check if it contains a 3 digit numeric number: 2xx, 4xx
  * or 5xx.
  * </ul>
  * 
  * @param str -
  *            message body
  * @param pass -
  *            1) first pass: look for RFC1893 token (x.x.x).
  *            2) second pass: look for RFC2821 token (xxx), must also match reply text.
  * @return bounce type or null if no RFC code is found.
  */
 private String scanBody(String body, int pass) {
  if (isDebugEnabled)
   logger.debug("Entering the examineBody method, pass " + pass);
  if (StringUtil.isEmpty(body)) { // sanity check
   return null;
  }
  BOUNCE_TYPES bounceType = null;
  if (pass == 1) {
   Matcher m = pattern1.matcher(StringUtil.cut(body, maxLenToScan));
   if (m.find()) { // only one time
    String token = m.group(m.groupCount());
    logger.info("examineBody(): RFC1893 token found: " + token);
    if ((bounceType = searchRfc1893CodeTable(token)) != null) {
     return bounceType.toString();
    }
    else if (token.startsWith("5.")) { // 5.x.x
     return BOUNCE_TYPES.HARD_BOUNCE.toString();
    }
    else if (token.startsWith("4.")) { // 4.x.x
     return BOUNCE_TYPES.SOFT_BOUNCE.toString();
    }
    else if (token.startsWith("2.")) { // 2.x.x
     // 2.x.x = OK message returned, MDN receipt.
     return BOUNCE_TYPES.MDN_RECEIPT.toString();
    }
   }
  }
  else if (pass == 2) {
   Matcher m = pattern2.matcher(StringUtil.cut(body, maxLenToScan));
   int end = 0;
   int count = 0;
   while (m.find(end) && count++ < 2) { // repeat two times
    String token = m.group(m.groupCount());
    end = m.end(m.groupCount());
    logger.info("examineBody(): Numeric token found: " + token);
    if ((bounceType = searchRfc2821CodeTable(token)) != null) {
     //return bounceType;
     return matchRfcText(bounceType, token, body, end);
    }
    if (token.startsWith("5")) {
     // 5xx = permanent failure, re-send will fail
     String r = matchRfcText(BOUNCE_TYPES.HARD_BOUNCE, token, body, end);
     if (r != null) return r;
     // else look for the second token
    }
    else if(token.equals("422")) {
     // 422 = mailbox full, re-send may be successful
     return matchRfcText(BOUNCE_TYPES.MAILBOX_FULL, token, body, end);
    }
    else if (token.startsWith("4")) {
     // 4xx = persistent transient failure, re-send may be successful
     String r = matchRfcText(BOUNCE_TYPES.SOFT_BOUNCE, token, body, end);
     if (r != null) return r;
     // else look for the second token
    }
    else if(token.startsWith("2")) {
     // 2xx = OK message returned.
    }
   }
  }
  return null;
 }

 /**
  * For RFC 2821, to further match reply text to prevent false positives.
  * 
  * @param bounceType -
  *            Bounce Type
  * @param code -
  *            RFC2821 code
  * @param tokens -
  *            message text stored in an array, each element holds a word.
  * @param idx -
  *            where the RFC2821 code located in the array
  * @return bounce type, or null if failed to match reply text.
  */
 private String matchRfcText(BOUNCE_TYPES bounceType, String code, String body, int idx) {
  String matchingText = RFC2821_STATUS_MATCHINGTEXT.get(code);
  if (matchingText == null) {
   if (code.startsWith("4")) {
    matchingText = RFC2821_STATUS_MATCHINGTEXT.get("4xx");
   }
   else if (code.startsWith("5")) {
    matchingText = RFC2821_STATUS_MATCHINGTEXT.get("5xx");
   }
   if (matchingText == null) { // just for safety
    return null;
   }
  }
  // RFC reply text - the first 120 characters after the RFC code 
  String rfcText = StringUtil.cut(body.substring(idx), 120);
  try {
   Pattern p = Pattern.compile(matchingText, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
   Matcher m = p.matcher(rfcText);
   if (m.find()) {
    logger.info("Match Succeeded: [" + rfcText + "] matched [" + matchingText + "]");
    return bounceType.toString();
   }
   else {
    logger.info("Match Failed: [" + rfcText + "] did not match [" + matchingText + "]");
   }
  }
  catch (PatternSyntaxException e) {
   logger.error("PatternSyntaxException caught", e);
  }
  return null;
 }
 
 /**
  * search smtp code table by RFC1893 token.
  * 
  * @param token
  *            DSN status token, for example: 5.0.0
  * @return message id related to the token
  */
 private BOUNCE_TYPES searchRfc1893CodeTable(String token) {
  // search rfc1893 hash table - x.x.x
  BOUNCE_TYPES bounceType = searchRfcCodeTable(token, RFC1893_STATUS_CODE);
  // search rfc1893 hash table - .x.x
  if (bounceType == null) {
   bounceType = searchRfcCodeTable(token.substring(1), RFC1893_STATUS_CODE);
  }
  return bounceType;
 }
 
 /**
  * search smtp code table by RFC token.
  * 
  * @param token -
  *            DSN status token, for example: 5.0.0, or 500 depending on the
  *            map used
  * @param map -
  *            either RFC1893_STATUS_CODE or RFC2821_STATUS_CODE
  * @return message id of the token
  */
 private BOUNCE_TYPES searchRfcCodeTable(String token, HashMap<String, String> map) {
  String type = map.get(token);

  if (type != null) { // found RFC status code
   logger.info("searchRfcCodeTable(): A match is found for type: " + type);
   if (type.equals(LETTER_H)) {
    return BOUNCE_TYPES.HARD_BOUNCE;
   }
   else if (type.equals(LETTER_S)) {
    return BOUNCE_TYPES.SOFT_BOUNCE;
   }
   else if (type.equals(LETTER_F)) {
    return BOUNCE_TYPES.MAILBOX_FULL;
   }
   else if (type.equals(LETTER_K)) {
    return BOUNCE_TYPES.MDN_RECEIPT;
   }
   else if (type.equals(LETTER_U)) {
    if (token.startsWith("4")) {
     return BOUNCE_TYPES.SOFT_BOUNCE;
    }
    else if (token.startsWith("5")) {
     return BOUNCE_TYPES.HARD_BOUNCE;
    }
   }
  }
  return null;
 }
 
 /**
  * search smtp code table by RFC token.
  * 
  * @param token -
  *            RFC2821 token, for example: 500
  * @return message id of the token
  */
 private BOUNCE_TYPES searchRfc2821CodeTable(String token) {
  return searchRfcCodeTable(token, RFC2821_STATUS_CODE);
 }
 
 /**
  * load the rfc1893 code table, from Rfc1893.properties file, into memory.
  * 
  * @throws IOException
  */
 private void loadRfc1893StatusCode() throws IOException {
  ClassLoader loader = this.getClass().getClassLoader();
  try {
   // read in RFC1893 status code file and store it in two property objects
   InputStream is = loader.getResourceAsStream("Rfc1893.properties");
   BufferedReader fr = new BufferedReader(new InputStreamReader(is));
   String inStr=null, code=null;
   while ((inStr = fr.readLine()) != null) {
    if (!inStr.startsWith("#")) {
     if (isDebugEnabled)
      logger.debug("loadRfc1893StatusCode(): " + inStr);
     StringTokenizer st = new StringTokenizer(inStr, "^\r\n");
     if (st.countTokens() == 3) {
      code = st.nextToken();
      RFC1893_STATUS_CODE.put(code, st.nextToken());
      RFC1893_STATUS_DESC.put(code, st.nextToken());
     }
     else if (st.countTokens() == 0) {
      // ignore
     }
     else {
      logger.fatal("loadRfc1893StatusCode(): Wrong record format: " + inStr);
     }
    }
   }
   fr.close();
  }
  catch (FileNotFoundException ex) {
   logger.fatal("file Rfc1893.properties does not exist", ex);
   throw ex;
  }
  catch (IOException ex) {
   logger.fatal("IOException caught during loading statcode.conf", ex);
   throw ex;
  }
 }

 /**
  * load the rfc2821 code table, from Rfc2821.properties file, into memory.
  * 
  * @throws IOException
  */
 private void loadRfc2821StatusCode() throws IOException {
  ClassLoader loader = this.getClass().getClassLoader();
  try {
   // read in RFC2821 status code file and store it in two property objects
   InputStream is = loader.getResourceAsStream("Rfc2821.properties");
   BufferedReader fr = new BufferedReader(new InputStreamReader(is));
   String inStr=null, code=null;
   while ((inStr = fr.readLine()) != null) {
    if (!inStr.startsWith("#")) {
     if (isDebugEnabled)
      logger.debug("loadRfc2821StatusCode(): " + inStr);
     StringTokenizer st = new StringTokenizer(inStr, "^\r\n");
     if (st.countTokens() == 3) {
      code = st.nextToken(); // 1st token = RFC code
      RFC2821_STATUS_CODE.put(code, st.nextToken()); // 2nd token = type
      String desc = st.nextToken(); // 3rd token = description
      RFC2821_STATUS_DESC.put(code, desc);
      // extract regular expression to be further matched
      String matchingRegex = getMatchingRegex(desc);
      if (matchingRegex != null) {
       RFC2821_STATUS_MATCHINGTEXT.put(code, matchingRegex);
      }
     }
     else if (st.countTokens() == 0) {
      // ignore
     }
     else {
      logger.fatal("loadRfc2821StatusCode(): Wrong record format: " + inStr);
     }
    }
   }
   fr.close();
  }
  catch (FileNotFoundException ex) {
   logger.fatal("file Rfc2821.properties does not exist", ex);
   throw ex;
  }
  catch (IOException ex) {
   logger.fatal("IOException caught during loading statcode.conf", ex);
   throw ex;
  }
 }
 
 private String getMatchingRegex(String desc) throws IOException {
  int left = desc.indexOf("{");
  if (left < 0) {
   return null;
  }
  Stack<Integer> stack = new Stack<Integer>();
  stack.push(Integer.valueOf(left));
  int nextPos = left;
  while (stack.size() > 0) {
   int leftPos = desc.indexOf("{", nextPos + 1);
   int rightPos = desc.indexOf("}", nextPos + 1);
   if (leftPos > rightPos) {
    if (rightPos > 0) {
     stack.pop();
     nextPos = rightPos;
    }
   }
   else if (leftPos < rightPos) {
    if (leftPos > 0) {
     nextPos = leftPos;
     stack.push(Integer.valueOf(leftPos));
    }
    else if (rightPos > 0) {
     stack.pop();
     nextPos = rightPos;
    }
   }
   else {
    break;
   }
  }
  if (nextPos > left) {
   if (stack.size() == 0) {
    return desc.substring(left + 1, nextPos);
   }
   else {
    logger.error("getMatchingRegex() - missing close curly brace: " + desc);
    throw new IOException("Missing close curly brace: " + desc);
   }
  }
  return null;
 }

 public static void main(String[] args) {
  try {
   SmtpScanner scan = SmtpScanner.getInstance();
   String bounceType = scan.scanBody("aaaaab\n5.0.0\nefg ");
   System.out.println("BounceType: " + bounceType);
   bounceType = scan.scanBody("aaa 201 aab\n422\naccount is full ");
   System.out.println("BounceType: " + bounceType);
   bounceType = scan.scanBody("aaaaab\n400\ntemporary failure ");
   System.out.println("BounceType: " + bounceType);
   System.out.println(scan.getMatchingRegex("{(?:mailbox|account).{0,180}(?:storage|full|limit|quota)}"));
  }
  catch (Exception e) {
   e.printStackTrace();
  }
 }
}

This class needs two additional property files to function, save them under the root folder of your classpath:

1) Rfc1893.properties:

# RFC1893/RFC3463 status code and description
# status code = class "." subject "." detail
# 2.x.x Success
# 4.x.x Persistent Transient Failure
# 5.x.x Permanent Failure
#
# format: StatusCode^Type^Description
# type = 
#
# permanent failure
5.0.0^h^Other undefined status
5.1.0^h^Other address status
5.1.1^h^Bad destination mailbox address
5.1.2^h^Bad destination system address
5.1.3^h^Bad destination mailbox address syntax
5.1.4^h^Destination mailbox address ambiguous
5.1.5^h^Destination mailbox address invalid (source: Microsoft)
5.1.6^h^Mailbox has moved
5.1.7^h^Bad sender's mailbox address syntax
5.1.8^h^Bad sender's system address
5.2.0^h^Other or undefined mailbox status
5.2.1^h^Mailbox disabled, not accepting messages
5.2.2^f^Mailbox full
5.2.3^l^Message length exceeds administrative limit.
5.2.4^h^Mailing list expansion problem
5.3.0^h^Other or undefined mail system status
5.3.1^s^Mail system full
5.3.2^h^System not accepting network messages
5.3.3^h^System not capable of selected features
5.3.4^l^Message too big for system
5.3.5^h^System incorrectly configured
5.4.0^h^Other or undefined network or routing status
5.4.1^h^No answer from host
5.4.2^h^Bad connection
5.4.3^h^Routing server failure
5.4.4^h^Unable to route
5.4.5^h^Network congestion
5.4.6^h^Routing loop detected
5.4.7^h^Delivery time expired
5.4.8^h^Loop detected, check recipient policy. (Source: Microsoft)
5.5.0^h^Other or undefined protocol status
5.5.1^h^Invalid command
5.5.2^h^Syntax error
5.5.3^h^Too many recipients
5.5.4^h^Invalid command arguments
5.5.5^h^Wrong protocol version
5.6.0^b^Other or undefined media error
5.6.1^b^Media not supported
5.6.2^h^Conversion required and prohibited
5.6.3^h^Conversion required but not supported
5.6.4^h^Conversion with loss performed
5.6.5^h^Conversion failed
5.7.0^h^Other or undefined security status
5.7.1^b^Delivery not authorized, message refused
5.7.2^h^Mailing list expansion prohibited
5.7.3^h^Security conversion required but not possible
5.7.4^h^Security features not supported
5.7.5^b^Cryptographic failure
5.7.6^b^Cryptographic algorithm not supported
5.7.7^b^Message integrity failure
# persistent transient failure
4.0.0^s^Other undefined status
4.1.0^s^Other address status
4.1.4^s^Destination mailbox address ambiguous
4.1.5^s^Destination mailbox address valid
4.1.7^s^Bad sender's mailbox address syntax
4.1.8^s^Bad sender's system address
4.2.0^s^Other or undefined mailbox status
4.2.1^s^Mailbox disabled, not accepting messages
4.2.2^f^Mailbox full
4.2.4^s^Mailing list expansion problem
4.3.0^s^Other or undefined mail system status
4.3.1^s^Mail system full
4.3.2^s^System not accepting network messages
4.3.3^s^System not capable of selected features
4.3.5^s^System incorrectly configured
4.4.0^s^Other or undefined network or routing status
4.4.1^s^No answer from host
4.4.2^s^Bad connection
4.4.3^s^Routing server failure
4.4.4^s^Unable to route
4.4.5^s^Network congestion
4.4.6^s^Routing loop detected
4.4.7^s^Delivery time expired
4.5.0^s^Other or undefined protocol status
4.5.3^s^Too many recipients
4.5.5^s^Wrong protocol version
4.6.0^s^Other or undefined media error
4.6.2^s^Conversion required and prohibited
4.6.3^s^Conversion required but not supported
4.6.4^s^Conversion with loss performed
4.6.5^s^Conversion failed
4.7.0^s^Other or undefined security status
4.7.5^s^Cryptographic failure
4.7.6^s^Cryptographic algorithm not supported
4.7.7^s^Message integrity failure
# generic entries
.0.0^s^Other undefined status
.1.0^s^Other address status
.1.1^h^Bad destination mailbox address
.1.2^h^Bad destination system address
.1.3^h^Bad destination mailbox address syntax
.1.4^h^Destination mailbox address ambiguous
.1.5^k^Destination mailbox address valid
.1.6^h^Mailbox has moved
.1.7^s^Bad sender's mailbox address syntax
.1.8^s^Bad sender's system address
.2.0^s^Other or undefined mailbox status
.2.1^h^Mailbox disabled, not accepting messages
.2.2^f^Mailbox full
.2.3^l^Message length exceeds administrative limit.
.2.4^s^Mailing list expansion problem
.3.0^s^Other or undefined mail system status
.3.1^s^Mail system full
.3.2^s^System not accepting network messages
.3.3^s^System not capable of selected features
.3.4^l^Message too big for system
.3.5^s^System incorrectly configured
.4.0^s^Other or undefined network or routing status
.4.1^s^No answer from host
.4.2^s^Bad connection
.4.3^s^Routing server failure
.4.4^s^Unable to route
.4.5^s^Network congestion
.4.6^s^Routing loop detected
.4.7^s^Delivery time expired
.5.0^s^Other or undefined protocol status
.5.1^h^Invalid command
.5.2^h^Syntax error
.5.3^s^Too many recipients
.5.4^h^Invalid command arguments
.5.5^s^Wrong protocol version
.6.0^s^Other or undefined media error
.6.1^s^Media not supported
.6.2^s^Conversion required and prohibited
.6.3^s^Conversion required but not supported
.6.4^s^Conversion with loss performed
.6.5^s^Conversion failed
.7.0^s^Other or undefined security status
.7.1^b^Delivery not authorized, message refused
.7.2^h^Mailing list expansion prohibited
.7.3^h^Security conversion required but not possible
.7.4^h^Security features not supported
.7.5^s^Cryptographic failure
.7.6^s^Cryptographic algorithm not supported
.7.7^s^Message integrity failure

2) Rfc2821.properties:

# RFC2821 reply code and description
# reply code = xyz
# 1yz Positive Preliminary reply
# 2yz   Positive Completion reply
# 3yz   Positive Intermediate reply
# 4yz   Transient Negative Completion reply
# 5yz   Permanent Negative Completion reply
#
# format: ReplyCode^Type^Description
# type = 
# Description: text enclosed in curly brackets should be further matched to prevent false positives.
#
211^k^System status, or system help reply
214^k^Help message
220^k^ Service ready
221^k^ Service closing transmission channel
250^k^Requested mail action okay, completed
251^k^User not local; will forward to 
252^k^Cannot VRFY user, but will accept message and attempt delivery
354^k^Start mail input; end with .
421^s^ Service not available, closing transmission channel {\bnot\s+available}
450^s^Requested mail action not taken: mailbox unavailable {\baction\s+not\s+taken}
451^s^Requested action aborted: local error in processing {\baction\s+aborted}
452^s^Requested action not taken: insufficient system storage {\baction\s+not\s+taken}
500^h^Syntax error, command unrecognized {\berror}
501^h^Syntax error in parameters or arguments {\berror}
502^h^Command not implemented {\bnot\s+implemented}
503^h^Bad sequence of commands {\bBad\s+sequence}
504^h^Command parameter not implemented {\bnot\s+implemented}
550^h^Requested action not taken: mailbox unavailable {\baction\s+not\s+taken}
551^h^User not local; please try  {\bnot\s+local}
552^f^Requested mail action aborted: exceeded storage allocation {\baction\s+aborted}
553^h^Requested action not taken: mailbox name not allowed {\baction\s+not\s+taken}
554^h^Transaction failed {\b(?:failed|delivery error)}
#
# *** Custom entries, not defined by RFC 2821 ***
#
422^f^{\b(?:mailbox|account)\b.{0,100}(?:storage|full|limit|quota)} mailbox full.
4xx^s^{\btemporary\s.{0,100}(?:failure|error)}, used to match undefined codes starting with 4
5xx^h^{\bpermanent\s.{0,100}(?:failure|error)}, used to match undefined codes starting with 5

Thursday, September 3, 2009

Detect bounced emails - Part 1

When an email is rejected by a mail server due to a violation such as user or mailbox not found, certain information are returned and included in either message headers or message body, or both.
There are many RFC's that address the rules and codes that should be included in the rejected messages.
For simplicity, we will only deal with RFC822, RFC1893, and RFC3464 here, which I believe are implemented in most if not all commercial and open source SMTP servers.
Please refer Portable java mail message bean as it is used by this bounce detecting program.
First we need a class to retrieve RFC822 Headers and Delivery Status Report from message bean, this class provides methods that travel through body part tree and collect RFC components and attachments. The RFC components are then used by a scanner (provided in Part 2) to find rejection reasons.

/*
 * blog/javaclue/javamail/BodypartUtil.java
 * 
 * Copyright (C) 2009 JackW
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package blog.javaclue.javamail;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.mail.Part;

import org.apache.log4j.Logger;

/**
 * provide utility methods to retrieve attachments from BodypartBean
 */
public final class BodypartUtil implements Serializable {
 private static final long serialVersionUID = -8920127339846912514L;
 static final Logger logger = Logger.getLogger(BodypartUtil.class);
 static final boolean isDebugEnabled = logger.isDebugEnabled();

 final static String LF = System.getProperty("line.separator", "\n");
 
 private BodypartUtil() {
 }
 
 /**
  * Retrieve all attachments into a list. It also looks for delivery status
  * report and rfc822 report. If it found a report, it saves the link to the
  * report to a MessageBean field so it can be easily retrieved later. Please
  * call this method before you call any other methods that will retrieve any
  * delivery reports.
  * 
  * @param msgBean -
  *            retrieving from
  * @return a list of MessageNode's
  */
 public static List<MessageNode> retrieveAttachments(MessageBean msgBean) {
  msgBean.setAttachments(retrieveAttachments((BodypartBean) msgBean, msgBean, 1));
  return msgBean.getAttachments();
 }

 private static List<MessageNode> retrieveAttachments(BodypartBean aNode, MessageBean msgBean,
   int level) {
  if (level <= 1) {
   msgBean.setRfc822(null);
   msgBean.setReport(null);
  }
  List<MessageNode> aNodes = new ArrayList<MessageNode>();
  Iterator<BodypartBean> it = aNode.getIterator();
  while (it.hasNext()) {
   BodypartBean subNode = it.next();
   String disp = subNode.getDisposition();
   String desc = subNode.getDescription();
   String mtype = subNode.getMimeType();
   String nullInd = subNode.getValue() == null ? "null" : "not null";
   logger.info("retrieveAttachments(): level=" + level + ", mtype=" + mtype + ", "
     + ", disp=" + disp + ", desc=" + desc + ", " + nullInd);
   if (Part.ATTACHMENT.equalsIgnoreCase(disp)
     || (Part.INLINE.equalsIgnoreCase(disp) && desc != null)
     || MessageBeanUtil.getFileName(subNode.getContentType()) != null) {
    
    // this Node is an attachment
    aNodes.add(new MessageNode(subNode, level));
   }
   // find other attachments down from the Node
   List<MessageNode> subAttch = retrieveAttachments(subNode, msgBean, level + 1);
   if (subAttch!=null) {
    aNodes.addAll(subAttch);
   }
   // save the node that contains status report
   if (mtype.startsWith("message/rfc822") && msgBean.getRfc822() == null)
    msgBean.setRfc822(new MessageNode(subNode, level));
   if (mtype.startsWith("multipart/report") && msgBean.getReport() == null)
    msgBean.setReport(new MessageNode(subNode, level));
  }
  // root node could also be multipart/report content type
  String mtype = aNode.getMimeType();
  if (mtype.startsWith("multipart/report") && msgBean.getReport() == null) {
   String disp = aNode.getDisposition();
   String desc = aNode.getDescription();
   logger.info("retrieveAttachments(): level=" + (level - 1) + ", mtype=" + mtype
     + ", disp=" + disp + ", desc=" + desc);
   msgBean.setReport(new MessageNode(aNode, level));
  }
  msgBean.setAttachments(aNodes);
  return msgBean.getAttachments();
 }

 public static List<BodypartBean> retrieveAlternatives(MessageBean msgBean) {
  List<BodypartBean> aNodes = null;
  if (msgBean.getContentType().startsWith("multipart/alternative")) {
   aNodes = msgBean.getNodes();
  }
  else if (msgBean.getContentType().startsWith("multipart/mixed")) {
   for (Iterator<BodypartBean> it = msgBean.getIterator(); it.hasNext();) {
    BodypartBean subNode = it.next();
    if (subNode.getContentType().startsWith("multipart/alternative")) {
     aNodes = subNode.getNodes();
    }
   }
  }
  if (aNodes == null) { // just for safety
   aNodes = new ArrayList<BodypartBean>();
  }
  if (aNodes.size() == 0) { // no alternatives, use body
   aNodes.add(msgBean);
  }
  return aNodes;
 }
 
 /**
  * retrieve RFC822 component into a BodypartBean format
  * @param aNode - root Node
  * @param level
  * @return a BodypartBean
  */
 public static BodypartBean retrieveRfc822Text(BodypartBean aNode, int level) {
  List<BodypartBean> sNode = aNode.getNodes();
  if (sNode != null && sNode.size() > 0) {
   // message/rfc822 attaches a text node as its child body part.
   BodypartBean subNode = sNode.get(0); // only the first node
   String mtype = subNode.getMimeType();
   String disp = subNode.getDisposition();
   logger.info("retrieveRFC822Text() - proceeded to level " + level + ", mtype="
     + mtype + ", disp=" + disp);
   if (mtype.startsWith("text")) {
    logger.info("retrieveRFC822Text() - found the child bodypart from level "
      + level);
    return subNode;
   }
   else { // go deeper to get the text node
    return retrieveRfc822Text(subNode, level + 1);
   }
  }
  else { // message/rfc822 contains no sub nodes
   logger.info("retrieveRFC822Text() - missing the lower level node, check if it's a text/rfc822-headers.");
   String mtype = aNode.getMimeType();
   String disp = aNode.getDisposition();
   logger.info("retrieveRFC822Text() - proceeded to level " + level + ", mtype="
     + mtype + ", disp=" + disp);
   if (mtype.startsWith("text/rfc822-headers")) {
    logger.info("retrieveRFC822Text() - found the text/rfc822-headers from level "
        + level);
    return aNode;
   }
   else {
    logger.info("retrieveRFC822Text() - missing the lower level node and rfc822-headers, use it anyway.");
    return aNode;
   }
  }
 }

 /**
  * retrieve rfc1894/rfc1891 component in a BodypartBean format
  * @param aNode - root Node
  * @param level
  * @return a BodypartBean
  */
 public static BodypartBean retrieveDlvrStatus(BodypartBean aNode, int level) {
  // multipart/report could attach a status node as its child body part.
  List<BodypartBean> sNode = aNode.getNodes();
  for (int i = 0; sNode != null && i < sNode.size(); i++) {
   BodypartBean subNode = sNode.get(i);
   String mtype = subNode.getMimeType();
   String disp = subNode.getDisposition();
   logger.info("retrieveDlvrStatus() - proceeded to level " + level + ", mtype="
     + mtype + ", disp=" + disp);
   if (mtype.startsWith("message/delivery-status")) {
    logger.info("retrieveDlvrStatus() - found message/delivery-status bodypart from level "
        + level);
    return subNode; // return delivery-status if one is found
   }
  }
  logger.info("retrieveDlvrStatus() - missing the lower level node or the lower level node has no text.");
  return null;
 }

 /**
  * retrieve rfc3798 component in a BodypartBean format
  * @param aNode - root Node
  * @param level
  * @return a BodypartBean
  */
 public static BodypartBean retrieveMDNReceipt(BodypartBean aNode, int level) {
  // multipart/report could attach a MDN node as its child body part.
  List<BodypartBean> sNode = aNode.getNodes();
  for (int i = 0; sNode != null && i < sNode.size(); i++) {
   BodypartBean subNode = sNode.get(i);
   String mtype = subNode.getMimeType();
   String disp = subNode.getDisposition();
   logger.info("retrieveMDNReceipt() - proceeded to level " + level + ", mtype=" + mtype
     + ", disp=" + disp);
   if (mtype.startsWith("message/disposition-notification")) {
    logger.info("retrieveMDNReceipt() - found message/disposition-notification "
      + "bodypart from level " + level);
    return subNode; // return disposition-notification if one is
        // found
   }
  }
  return null;
 }

 /**
  * retrieve rfc1894/rfc1891 components in a BodypartBean format
  * @param aNode - root Node
  * @param level
  * @return a BodypartBean list
  */
 public static List<BodypartBean> retrieveReportText(BodypartBean aNode, int level) {
  // multipart/report could attach text nodes as its sub body parts.
  List<BodypartBean> sNode = aNode.getNodes();
  List<BodypartBean> list = new ArrayList<BodypartBean>();
  for (int i = 0; sNode != null && i < sNode.size(); i++) {
   BodypartBean subNode = sNode.get(i);
   String mtype = subNode.getMimeType();
   String disp = subNode.getDisposition();
   logger.info("retrieveReportText() - proceeded to level " + level + ", mtype="
     + mtype + ", disp=" + disp);
   if (mtype.startsWith("text") && !mtype.startsWith("text/rfc822-headers")) {
    logger.info("retrieveReportText() - found " + mtype + " bodypart from level "
      + level);
    list.add(subNode);
   }
  }
  return list;
 }

 /**
  * retrieve rfc822 message in a BodypartBean format
  * @param aNode - root Node
  * @param level
  * @return a BodypartBean
  */
 public static BodypartBean retrieveMessageRfc822(BodypartBean aNode, int level) {
  // locate message/rfc822 section under multipart/report
  List<BodypartBean> sNode = aNode.getNodes();
  for (int i = 0; sNode != null && i < sNode.size(); i++) {
   BodypartBean subNode = sNode.get(i);
   String mtype = subNode.getMimeType();
   String disp = subNode.getDisposition();
   logger.info("retrieveMessageRFC822() - proceeded to level " + level + ", mtype="
     + mtype + ", disp=" + disp);
   if (mtype.startsWith("message/rfc822")) {
    logger.info("retrieveMessageRFC822() - found message/rfc822 section from level "
      + level);
    return subNode;
   }
  }
  return null;
 }

 /**
  * retrieve RFC822 headers component in a BodypartBean format
  * @param aNode - root Node
  * @param level
  * @return a BodypartBean
  */
 public static BodypartBean retrieveRfc822Headers(BodypartBean aNode, int level) {
  // locate text/rfc822-headers section under multipart/report
  List<BodypartBean> sNode = aNode.getNodes();
  for (int i = 0; sNode != null && i < sNode.size(); i++) {
   BodypartBean subNode = sNode.get(i);
   String mtype = subNode.getMimeType();
   String disp = subNode.getDisposition();
   logger.info("retrieveRFC822Headers() - proceeded to level " + level + ", mtype="
     + mtype + ", disp=" + disp);
   if (mtype.startsWith("text/rfc822-headers")) {
    logger.info("retrieveRFC822Headers() - found message/rfc822 section from level "
      + level);
    return subNode;
   }
  }
  return null;
 }
}

Followers

About Me

An IT professional with more than 20 years of experience in enterprise computing. An Audio enthusiast designed and built DIY audio gears and speakers.