Advanced java code dealing with real world problems.

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

Portable java mail message bean - Part 5

This is the last part of the series, in which it provides a string manipulation utility class that is needed by the conversion utility class presented in Part 4.
/*
 * blog/javaclue/javamail/StringUtil.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.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.Address;

import org.apache.log4j.Logger;

public final class StringUtil {
 static final Logger logger = Logger.getLogger(StringUtil.class);
 static final boolean isDebugEnabled = logger.isDebugEnabled();

 public final static String TOKEN_XHDR_BEGIN = "10.";
 public final static String TOKEN_XHDR_END = ".0";

    private StringUtil() {
    }

 public static boolean isEmpty(String str) {
  return (str == null || str.trim().length() == 0);
 }

 /**
  * trim the input string from the right to the specified length.
  * 
  * @param str -
  *            original string
  * @param len -
  *            string size
  * @return string trimmed to the size of "len"
  */
 public static String cut(String str, int len) {
  if (str == null || str.length() <= len || len < 0)
   return str;
  else
   return str.substring(0, len);
 }

 /**
  * trim the input string from the right to the specified length.
  * 
  * @param str -
  *            original string
  * @param len -
  *            string size
  * @return trimmed string plus three dots if its size is over specified length.
  */
 public static String cutWithDots(String str, int len) {
  if (str == null || str.length() <= len || len < 0)
   return str;
  else if (str.length() > len)
   return str.substring(0, len) + "...";
  else
   return str;
 }

 /**
  * remove double and single quotes from input string.
  * 
  * @param data -
  *            input string
  * @return string with quotes removed, or null if input is null
  */
 public static String removeQuotes(String data) {
  if (data == null) return data;
  StringTokenizer st = new StringTokenizer(data, "\"\'");
  StringBuffer sb = new StringBuffer();
  while (st.hasMoreTokens())
   sb.append(st.nextToken());

  return sb.toString();
 }

 /**
  * convert Address array to a string, addresses are comma delimited. Display
  * names are removed from returned addresses.
  * 
  * @param addrs -
  *            Address array
  * @return a string of addresses, comma delimited. or null if input is
  *         null
  */
 public static String addrToString(Address[] addrs) {
  return addrToString(addrs, true);
 }

 /**
  * convert Address array to string, addresses are comma delimited.
  * 
  * @param addrs -
  *            Address array
  * @param removeDisplayName -
  *            remove display name from addresses if true
  * @return a string of addresses, comma delimited. or null if input is
  *         null
  */
 public static String addrToString(Address[] addrs, boolean removeDisplayName) {
  if (addrs == null || addrs.length == 0) {
   return null;
  }
  String str = addrs[0].toString();
  if (removeDisplayName) {
   str = removeDisplayName(str);
  }
  for (int i = 1; i < addrs.length; i++) {
   if (removeDisplayName) {
    str = str + "," + removeDisplayName(addrs[i].toString());
   }
   else {
    str = str + "," + addrs[i].toString();
   }
  }
  return str;
 }

 /**
  * remove display name from an email address, and convert all characters 
  * to lower case.
  * 
  * @param addr -
  *            email address
  * @return email address without display name, or null if input is null.
  */
 public static String removeDisplayName(String addr) {
  return removeDisplayName(addr, true);
 }

 /**
  * remove display name from an email address.
  * 
  * @param addr -
  *            email address
  * @param toLowerCase -
  *            true to convert characters to lower case
  * @return email address without display name, or null if input is null.
  */
 public static String removeDisplayName(String addr, boolean toLowerCase) {
  if (isEmpty(addr)) {
   return addr;
  }
  int at_pos = addr.lastIndexOf("@");
  if (at_pos > 0) {
   int pos1 = addr.lastIndexOf("<", at_pos);
   int pos2 = addr.indexOf(">", at_pos + 1);
   if (pos1 >= 0 && pos2 > pos1) {
    addr = addr.substring(pos1 + 1, pos2);
   }
  }
  if (toLowerCase)
   return addr.toLowerCase();
  else
   return addr;
 }

 /**
  * check if an email address has a display name.
  * 
  * @param addr -
  *            email address
  * @return true if it has a display name
  */
 public static boolean hasDisplayName(String addr) {
  if (isEmpty(addr)) return false;
  return addr.matches("^\\s*\\S+.{0,250}\\<.+\\>\\s*$");
 }

 /**
  * return the display name of an email address.
  * 
  * @param addr -
  *            email address
  * @return - display name of the address, or null if the email address does
  *         not have a display name.
  */
 public static String getDisplayName(String addr) {
  if (isEmpty(addr)) {
   return null;
  }
  int at_pos = addr.lastIndexOf("@");
  if (at_pos > 0) {
   int pos1 = addr.lastIndexOf("<", at_pos);
   int pos2 = addr.indexOf(">", at_pos + 1);
   if (pos1 >= 0 && pos2 > pos1) {
    String dispName = addr.substring(0, pos1);
    return dispName.trim();
   }
  }
  return null;
 }

 /**
  * Compare two email addresses. Email address could be enclosed by angle
  * brackets and it should still be equal to the one without angle brackets.
  * 
  * @param addr1 -
  *            email address 1
  * @param addr2 -
  *            email address 2
  * @return 0 if addr1 == addr2, -1 if addr1 < addr2, or 1 if addr1 > addr2.
  */
 public static int compareEmailAddrs(String addr1, String addr2) {
  if (addr1 == null) {
   if (addr2 != null) {
    return -1;
   }
   else {
    return 0;
   }
  }
  else if (addr2 == null) {
   return 1;
  }
  addr1 = removeDisplayName(addr1, true);
  addr2 = removeDisplayName(addr2, true);
  return addr1.compareToIgnoreCase(addr2);
 }

 /**
  * returns the domain name of an email address.
  * 
  * @param addr -
  *            email address
  * @return domain name of the address, or null if it's a local address
  */
 public static String getEmailDomainName(String addr) {
  if (isEmpty(addr)) {
   return null;
  }
  int pos;
  if ((pos = addr.lastIndexOf("@")) > 0) {
   String domain = addr.substring(pos + 1).trim();
   if (domain.endsWith(">")) {
    domain = domain.substring(0, domain.length() - 1);
   }
   return (domain.length() == 0 ? null : domain);
  }
  return null;
 }

  /**
  * Strip off leading and trailing spaces for all String objects on a list.
  * 
  * @param list -
  *            a list of objects
  */
 public static void stripAll(ArrayList<Object> list) {
  if (list==null) return;
  for (int i=0; i<list.size(); i++) {
   Object obj = list.get(i);
   if (obj!=null && obj instanceof String)
    list.set(i,((String)obj).trim());
  }
 }

 /**
  * Strip off leading and trailing spaces for all String objects in an array.
  * 
  * @param array -
  *            an array of objects
  */
 public static void stripAll(Object[] array) {
  if (array==null) return;
  for (int i=0; i<array.length; i++) {
   Object obj = array[i];
   if (obj!=null && obj instanceof String)
    array[i]=((String)obj).trim();
  }
 }

 /**
  * For String objects found in the bean class with a getter and a setter,
  * this method will strip off the leading and trailing spaces of those
  * string objects.
  * 
  * @param obj -
  *            a bean object
  */
 public static void stripAll(Object obj) {
  if (obj == null) {
   return;
  }
  Method methods[] = obj.getClass().getDeclaredMethods();
  try {
   Class<?> setParms[] = { Class.forName("java.lang.String") };
   for (int i = 0; i < methods.length; i++) {
    Method method = (Method) methods[i];
    Class<?> parmTypes[] = method.getParameterTypes();
    int mod = method.getModifiers();
    if (Modifier.isPublic(mod) && !Modifier.isAbstract(mod) && !Modifier.isStatic(mod)) {
     if (method.getName().startsWith("get") && parmTypes.length == 0
       && method.getReturnType().getName().equals("java.lang.String")) {
      // invoke the get method
      String str = (String) method.invoke(obj, (Object[])parmTypes);
      if (str != null) { // trim the string
       String setMethodName = "set" + method.getName().substring(3);
       try {
        Method setMethod = obj.getClass()
          .getMethod(setMethodName, setParms);
        if (setMethod != null) {
         String strParms[] = { str.trim() };
         setMethod.invoke(obj, (Object[])strParms);
        }
       }
       catch (Exception e) {
        // no corresponding set method, ignore.
       }
      }
     }
    }
   }
  }
  catch (Exception e) {
   System.err.println("ERROR: Exception caught during reflection - " + e);
   e.printStackTrace();
  }
 }

 /**
  * replace all occurrences of replFrom with replWith in a string.
  * 
  * @param body -
  *            message text
  * @param replFrom -
  *            from string
  * @param replWith -
  *            with string
  * @return new string
  */
 public static String replaceAll(String body, String replFrom, String replWith) {
  if (body == null || body.trim().length() == 0) {
   return body;
  }
  if (replFrom == null || replWith == null) {
   logger.warn("replaceAll() - either replFrom or replyWith is null.");
   return body;
  }
  StringBuffer sb = new StringBuffer();
  int newpos = 0, pos = 0;
  while ((newpos = body.indexOf(replFrom, pos)) >= 0) {
   sb.append(body.substring(pos, newpos));
   sb.append(replWith);
   pos = newpos + Math.max(1, replFrom.length());
  }
  sb.append(body.substring(pos, body.length()));
  return sb.toString();
 }

 /**
  * remove the first occurrence of the given string from a string.
  * 
  * @param body -
  *            original body
  * @param removeStr -
  *            string to be removed
  * @return new body
  */
 public static String removeFirstString(String body, String removeStr) {
  return removeString(body, removeStr, false);
 }

 /**
  * remove the last occurrence of the given string from a string.
  * 
  * @param body -
  *            original body
  * @param removeStr -
  *            string to be removed
  * @return new body
  */
 public static String removeLastString(String body, String removeStr) {
  return removeString(body, removeStr, true);
 }

 private static String removeString(String body, String removeStr, boolean removeLast) {
  if (body == null || body.trim().length() == 0) {
   return body;
  }
  if (removeStr == null || removeStr.trim().length() == 0) {
   return body;
  }
  int pos = -1;
  if (removeLast) {
   pos = body.lastIndexOf(removeStr);
  }
  else {
   pos = body.indexOf(removeStr);
  }
  if (pos >= 0) {
   body = body.substring(0, pos) + body.substring(pos + removeStr.length());
  }
  return body;
 }

 /**
  * returns a string of periods.
  * 
  * @param level -
  *            number periods to be returned
  * @return string of periods
  */
 public static String getPeriods(int level) {
  StringBuffer sb = new StringBuffer();
  for (int i = 0; i < level; i++) {
   sb.append(".");
  }
  return sb.toString();
 }

 public static String removeCRLFTabs(String str) {
  // remove possible CR/LF and tabs, that are inserted by some Email
  // servers, from the Email_ID found in bounced E-mails (MS exchange
  // server for one). MS exchange server inserted "\r\n\t" into the
  // Email_ID string, and it caused "check digit test" error.
  StringTokenizer sTokens = new StringTokenizer(str, "\r\n\t");
  StringBuffer sb = new StringBuffer();
  while (sTokens.hasMoreTokens()) {
   sb.append(sTokens.nextToken());
  }
  return sb.toString();
 }

    private final static String localPart = "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*";
 private final static String remotePart = "@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])+";
 private final static String intraPart = "@[a-z0-9](?:[a-z0-9-]*[a-z0-9])+";
 
    private final static Pattern remotePattern = Pattern.compile("^" + localPart + remotePart + "$",
   Pattern.CASE_INSENSITIVE);
    private final static Pattern intraPattern = Pattern.compile("^" + localPart + intraPart + "$",
   Pattern.CASE_INSENSITIVE);
  private static final Pattern localPattern = Pattern.compile("^" + localPart + "$",
   Pattern.CASE_INSENSITIVE);
  
  public static String getEmailRegex() {
   return localPart + remotePart;
  }
 
    /**
  * Check if the provided string is a valid email address. This conforms to
  * the RFC822 and RFC1035 specifications. Both local part and remote part
  * are required.
  * 
  * @param string
  *            The string to be checked.
  * @return True if string is an valid email address. False if not.
  */
    public static boolean isRemoteEmailAddress(String string) {
     if (string == null) return false;
     Matcher matcher = remotePattern.matcher(string);
     return matcher.matches();
    }

   /**
  * Check if the provided string is a valid remote or Intranet email address.
  * An Intranet email address could include only a sub-domain name such as
  * "bounce" or "localhost" as its remote part.
  * 
  * @param string
  *            The string to be checked.
  * @return True if string is an valid email address. False if not.
  */
    public static boolean isRemoteOrIntranetEmailAddress(String string) {
     if (string == null) return false;
     if (isRemoteEmailAddress(string)) return true;
     Matcher matcher = intraPattern.matcher(string);
     return matcher.matches();
    }

 /**
  * matches any remote or local email addresses like john (without a remote
  * part) or john@localhost or john@smith.com.
  * 
  * @param string
  *            the email address to be checked
  * @return true if it's a valid email address
  */
 public static boolean isRemoteOrLocalEmailAddress(String string) {
  if (string == null) return false;
  if (isRemoteOrIntranetEmailAddress(string)) return true;
  Matcher matcher = localPattern.matcher(string);
  return matcher.matches();
 }

    public static boolean isValidEmailLocalPart(String string) {
     Matcher matcher = localPattern.matcher(string);
     return matcher.matches();
    }

 
 private static String bounceRegex = (new StringBuilder("\\s*\\W?((\\w+)\\-(")).append(
   TOKEN_XHDR_BEGIN).append("\\d+").append(TOKEN_XHDR_END).append(
   ")\\-(.+\\=.+)\\@(.+\\w))\\W?\\s*").toString();
  // for ex.: bounce-10.07410251.0-jsmith=test.com@localhost
 private static Pattern bouncePattern = Pattern.compile(bounceRegex);
 private static String removeRegex = "\\s*\\W?((\\w+)\\-(\\w+)\\-(.+\\=.+)\\@(.+\\w))\\W?\\s*";
  // for ex.: remove-testlist-jsmith=test.com@localhost
 private static Pattern removePattern = Pattern.compile(removeRegex);
 
 public static boolean isVERPAddress(String recipient) {
  if (isEmpty(recipient)) {
   return false;
  }
  Matcher bounceMatcher = bouncePattern.matcher(recipient);
  Matcher removeMatcher = removePattern.matcher(recipient);
  return bounceMatcher.matches() || removeMatcher.matches();
 }
 
 public static String getDestAddrFromVERP(String verpAddr) {
  Matcher bounceMatcher = bouncePattern.matcher(verpAddr);
  if (bounceMatcher.matches()) {
   if (bounceMatcher.groupCount() >= 5) {
    String destAddr = bounceMatcher.group(2) + "@" + bounceMatcher.group(5);
    return destAddr;
   }
  }
  Matcher removeMatcher = removePattern.matcher(verpAddr);
  if (removeMatcher.matches()) {
   if (removeMatcher.groupCount() >= 5) {
    String destAddr = removeMatcher.group(2) + "@" + removeMatcher.group(5);
    return destAddr;
   }
  }
  return verpAddr;
 }
 
 public static String getOrigAddrFromVERP(String verpAddr) {
  Matcher bounceMatcher = bouncePattern.matcher(verpAddr);
  if (bounceMatcher.matches()) {
   if (bounceMatcher.groupCount() >= 4) {
    String origAddr = bounceMatcher.group(4).replace('=', '@');
    return origAddr;
   }
  }
  Matcher removeMatcher = removePattern.matcher(verpAddr);
  if (removeMatcher.matches()) {
   if (removeMatcher.groupCount() >= 4) {
    String origAddr = removeMatcher.group(4).replace('=', '@');
    return origAddr;
   }
  }
  return verpAddr;
 }
 
 public static void main(String[] args) {
  String addr = "\"ORCPT foobar@nc.rr.com\" <foobar@nc.rr.com>";
  addr = "DirectStarTV <fqusoogd.undlwfeteot@chaffingphotosensitive.com>";
  System.out.println(addr+" --> "+removeDisplayName(addr));
  
  System.out.println(removeFirstString("<pre>12345abcdefklqhdkh</pre>", "<pre>"));
  System.out.println("EmailAddr: " + isRemoteEmailAddress("A!#$%&'*+/=?.^_`{|}~-BC@localhost.us"));
  System.out.println("EmailAddr: " + isRemoteOrLocalEmailAddress("A!#$%&'*+/=?.^_`{|}~-BC"));
  System.out.println(getOrigAddrFromVERP("bounce-10.07410251.0-jsmith=test.com@localhost"));
  System.out.println(getOrigAddrFromVERP("remove-testlist-jsmith=test.com@localhost"));
 }
}

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.