Advanced java code dealing with real world problems.

Tuesday, September 29, 2015

My Windows 10 upgrade experience - a total failure

Within the first couple of days the Windows 10 was launched, I upgraded 2 desktops and 2 laptops from Windows 7 to Windows 10.

The first desktop was my home theater PC, which has a tuner card and a TV media program installed. After the upgrade, I was surprised to see that the tuner card and the media program were still working well, along with every other applications I had installed before the upgrade.

The first laptop that went through the upgrade was my internet music box. It continued to play the music after the upgrade. I then upgraded another laptop that I use primarily in my bedroom and that went well too.

Encouraged by the successes, I proceeded to upgrade my son's gaming computer but it did not go well as my other computers. It would lock up and freeze all the time. I had to roll it back to Windows 7. The Windows 10's built-in "Go Back" function failed to restore the system. But since I have kept a True Image backup copy and I was able to restore my son's computer back to Windows 7.

Then on a midnight, or maybe early morning around 3 am, my internet music laptop started to playing music on its own. It turned out that Windows 10 will wake up the computer to perform windows updates on its own. And it cannot be turned off. The only solution was to roll it back to Windows 7.

Then it was my bedroom laptop that started to wake me up at midnight, again by the windows updates program. So it went back to Windows 7 too.

By then there was only one computer still running Windows 10, my HTPC, Until today, today I turned it on to watch a live TV and found out the tuner program has stopped working.

Lessons learned. If you are planning on the upgrade, make a copy of your current system and make sure you can recover your system from the copy.


Friday, May 4, 2012

SOAP Web Service using Spring-WS 2.0 - Part 1

Spring Web Service framework has come a long way, and it has made implementing web services in java a trivial task with release 2.0. Let's walk-through a creation of a simple "Project Search" web service project, step by step.

Step 1) create a web project with your favorite IDE, I use Eclipse or IBM RAD.

Step 2) create or modify your web.xml with following entries:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
            <!-- classpath*:spring-ws-test.xml, -->
    </param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
 
<servlet>
    <description>Spring Message Dispatcher</description>
    <display-name>ws-test</display-name>
    <servlet-name>ws-test</servlet-name>
    <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    <init-param>
        <param-name>transformWsdlLocations</param-name>
        <param-value>true</param-value>
    </init-param>
</servlet>

 

<servlet-mapping>
    <servlet-name>ws-test</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

Step 3) create an xml schema that defines service request and response message format, and save it as ws-project.xsd under WEB-INF folder:

<schema xmlns="http://www.w3.org/2001/XMLSchema"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:tns="http://javaclue.blogger.com/ws-project"
    targetNamespace="http://javaclue.blogger.com/ws-project"
    elementFormDefault="qualified">

    <xs:element name="ProjectRequest" type="tns:ProjectRequestType"/>
    <xs:element name="ProjectResponse" type="tns:ProjectResponseType"/>

    <xs:complexType name="ProjectRequestType">
        <xs:sequence>
            <xs:element name="Name" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="ProjectResponseType">
        <xs:sequence>
            <xs:element name="Name" type="xs:string"/>
            <xs:element name="Description" type="xs:string"/>
            <xs:element name="Url" type="xs:string"/>
            <xs:element name="StartDate" type="xs:date"/>
            <xs:element name="EndDate" type="xs:date"/>
        </xs:sequence>
    </xs:complexType>
</schema>

Step 4) create a Spring Web Service configuration file called "ws-test-servlet.xml" and save it under WEB-INF folder. Notice that the "servlet-name" in web.xml is used to name this Spring web service configuration xml file, this is required by Spring-WS framework.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:sws="http://www.springframework.org/schema/web-services"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<context:component-scan base-package="javaclue.ws"/>
<sws:annotation-driven/>

<sws:dynamic-wsdl id="projectSearch"
    portTypeName="ProjectSearch"
    locationUri="/SpringWSTest/" requestSuffix="Request" responseSuffix="Response"
    targetNamespace="http://javaclue.blogger.com/ws-project">
  <sws:xsd location="/WEB-INF/ws-project.xsd"/>
</sws:dynamic-wsdl>
</beans>

In this config file, we tell Spring where our web service schema is located, and Spring will generate WSDL based on the xsd file. Please also notice that we have enabled "component-scan" and "annotation-driven" in the config file, this is required for our project.

Once our project is deployed to a servlet container, for example a local tomcat server, we should be able to access the wsdl file from browser with address: http://localhost:8080/SpringWSTest/projectSearch.wsdl

Step 5) create an Endpoint class that will be used to serve the request. For simplicity, we display the request xml to the console and load a static xml from a file and return it as response. The class is annotated with @Endpoint, and service method is annotated with @PayloadRoot:

package javaclue.ws.endpoint;

import java.io.IOException;
import java.io.InputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;
import org.jdom2.input.DOMBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Spring-WS End-Points are scoped as singleton by default and have to be thread safe.
 */
@Endpoint
public class ProjectSearchEndpoint {
    static Logger logger = Logger.getLogger(ProjectSearchEndpoint.class);
   
    @PayloadRoot(namespace = "http://javaclue.blogger.com/ws-project", localPart = "ProjectRequest")
    @ResponsePayload
    public Element searchProjects(@RequestPayload Element request) {
        // print out xml payload using jdom
        XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
        DOMBuilder builder = new DOMBuilder();
        org.jdom2.Element doc = builder.build(request);
        try {
            xout.output(doc, System.out);
        }
        catch (IOException e) {
            logger.error("Exception", e); // put your error handling logic here
        }
       
        // load response from xml file and return it
        try {
            Document resp = loadDocumentFromFilePath("/TestXmls/TestResponse.xml");
            return resp.getDocumentElement();
        }
        catch (Exception e) {
            logger.error("Exception", e); // put your error handling logic here
        }
        return null;
    }

    private Document loadDocumentFromFilePath(String doc_path)
            throws ParserConfigurationException, SAXException, IOException {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        InputStream doc_is = loader.getResourceAsStream(doc_path);
        if (doc_is == null) {
            throw new IllegalArgumentException("Could not find xml file: " + doc_path);
        }
        // use jaxp to initialize a DOM parser
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false);
        factory.setNamespaceAware(true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse(new InputSource(doc_is));
        return document;
    }
}

Step 6) to make above Endpoint work, create a folder called "TestXmls" in your project's src (source) folder, and save following sample xml as "TestResponse.xml" to the folder. In real world you will need to construct a real response based on the request and return it.

<tns:ProjectResponse
    xsi:schemaLocation="http://javaclue.blogger.com/ws-project ws-project.xsd"
    xmlns:tns="http://javaclue.blogger.com/ws-project"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <tns:Name>SpringWSTest</tns:Name>
    <tns:Description>Test project using Spring-WS 2.0</tns:Description>
    <tns:Url>http://localhost:8080/SpringWSTest/projectSearch.wsdl</tns:Url>
    <tns:StartDate>2011-01-01</tns:StartDate>
    <tns:EndDate>2012-01-30</tns:EndDate>
</tns:ProjectResponse>

Step 7) deploy the project to your favorite container. And it's ready to serve.

Friday, September 24, 2010

Java COBOL data exchange - Quick Start Guide

Java has rapidly grown into enterprise landscape and the needs for integrating with mainframe world through JMS/MQ or CICS Transaction Gateway has become a critical piece to many enterprise IT stacks.

JavaCobolExchanger - an open source google code project is developed to address the needs.

Here is the link to the project: http://code.google.com/a/eclipselabs.org/p/java-cobol-exchanger/

To get started first we need to understand COBOL copy books. A copy book is a data description of data layout in fixed format. For example:

01 MY-EXCHANGE-RECORD.
  05 USER-ID          PIC X(20).
  05 USER-AGE         PIC 9(3).
  05 USER_NAME.
    10 FIRST-NAME     PIC X(20).
    10 LAST-NAME      PIC X(20).
    10 MIDDLE_INITIAL PIC X(1).
  05 AMOUNT-RECEIVED  PIC 9(5).99.
  05 DATE-REGISTERED.
    10 DATE-CCYY      PIC 9(4).
    10 DATE-SEP1      PIC X.
    10 DATE-MM        PIC 9(2).
    10 DATE-SEP2      PIC X.
    10 DATE-DD        PIC 9(2).

This copy book tells us that the first 20 characters are reserved for user Id, the next 3 characters are reserved for user age and it must be numeric. The next 41 characters are reserved for a group called USER-NAME and the group contains three elements. Followed after the group is a decimal element with equivalent decimal format of  "00000.00". The last 10 characters are reserved for a date element, which again is a group consisting of five elements, with equivalent date format of "yyyy-MM-dd".

To construct a java exchange instance of above COBOL copy book:

public class CobolCopybook extends ExchangeRecord {

    public CobolCopybook() {
        // define user name group
        BaseElement userNameGroup[] = {
                new StringElement("firstName", 20),
                new StringElement("lastName", 20),
                new StringElement("middleInitial",1),
        };
    
        // define the exchange record
        list.add(new StringElement("userId", 20));
        list.add(new IntegerElement("userAge", 3));
        list.add(new StructElement("userName", userNameGroup));
        list.add(new DecimalElement("amount", "00000.00"));
        list.add(new DateTimeElement("date", "yyyy-MM-dd"));
    }
}

To load the instance with data, simply add the following code:


ExchangeRecord bean = new CobolCopybook();
bean.getElement("userId").setValue("test user");
bean.getElement("userAge").setValue("35");
StructElement userNameGroup = (StructElement) bean.getElement("userName");
userNameGroup.getElement("firstName").setValue("John");
userNameGroup.getElement("lastName").setValue("Smith");
bean.getElement("amount").setValue("199.99");
bean.getElement("date").setValue("2010-01-01");






Now you can export the instance to a fixed format string which can then be consumed by a COBOL program:
 
String cobolString = bean.exportToString();
// XXX: add your code here to send it to mainframe

The data exchange can be performed in either direction, we just demonstrated a data exchange from java to COBOL. Now let's see how we handle exchange from a COBOL output:

First we need create an java exchange instance:

ExchangeRecord bean = new ComplexCopybook();

Then we import the COBOL output to the instance:

bean.importFromString();

And now we can access the COBOL data using getValue method:

String userId = bean.getElement("userId").getValue();
Integer userAge = (Integer) bean.getElement("userAge").getValue();

Thursday, September 2, 2010

A fast XML schema validator

JAXP 1.3 introduced a SchemaFactory by which you can compile a schema from a xsd file, and use the compiled schema to create a Validator that can be used to validate a XML document. Since the schema compilation takes some time, it would be beneficial in a service oriented environment to reuse the compiled schemas for future requests. Presented here is a simple SchemaValidator class that implements the idea with a hash table.

 /*
 * blog/javaclue/xml/SchemaValidator.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.xml;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Hashtable;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * Validate XML against schema using SchemaFactory (JAXP 1.3).
 * Schema compilation usually takes time, this class saves the compiled schemas
 * in a hash table so they can be reused by future requests and other instances.
 */
public class SchemaValidator {
    protected static Logger logger = Logger.getLogger(SchemaValidator.class);
    protected static boolean isDebugEnabled = logger.isDebugEnabled();
  
    private final String schema_path;
    private final String schema_file;
    private static final Map schemaPool = new Hashtable();
  
    public SchemaValidator(String schema_path, String schema_file) {
        this.schema_path = schema_path;
        this.schema_file = schema_file;
    }

    /**
     * Validate xmlStream against schema
     *
     * @throws IOException
     * @throws SAXException
     * @throws ParserConfigurationException
     */
    public void validate(Document xml_doc) throws ParserConfigurationException, SAXException,
            IOException {
        validate(xml_doc.getDocumentElement());
    }

    public void validate(Element element) throws SAXException, IOException {
        Schema schema = compileSchema(schema_path + schema_file);
      
        // create a Validator instance, which can be used to validate an
        // instance document
        Validator validator = schema.newValidator();
        validator.setErrorHandler(new SaxErrorHandler());
      
        // validate the DOM tree
        validator.validate(new DOMSource(element));
    }

    private static Schema compileSchema(String schemaFile) throws SAXException {
        if (!schemaPool.containsKey(schemaFile)) {
            if (isDebugEnabled) {
                logger.info("compileSchema() - compile schema file: " + schemaFile);
            }
            // create a SchemaFactory capable of understanding W3C schemas
            SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

            factory.setErrorHandler(new SaxErrorHandler());
           
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            URL url = loader.getResource(schemaFile);
            if (url == null) {
                throw new RuntimeException("Could not find Schema file: " + schemaFile);
            }
            InputStream is = loader.getResourceAsStream(schemaFile);
            // load a schema, represented by a Schema instance
            Source schemasrc = new StreamSource(is, url.getPath());
            Schema schema = factory.newSchema(schemasrc);
            schemaPool.put(schemaFile, schema);
        }
        return schemaPool.get(schemaFile);
    }

    public static synchronized void removeFromPool(String schemaFile) {
        schemaPool.remove(schemaFile);
    }

    public static synchronized void clearPool() {
        schemaPool.clear();
    }
  
    public static void main(String[] args) {
        SchemaValidator test = new SchemaValidator("Schemas/", "MySampleSchema.xsd");
        try {
            Document doc = XMLHelper.loadDocument("Test_xmls/MySampleXml.xml");
            test.validate(doc);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Simple IBM MQ Message Age Monitor

For those that are still running IBM MQ (or Websphere MQ) on mainframes, presented here is a simple java message age monitor program that polls a queue periodically for how long the oldest message has been living in the queue. This sample code will output a message to the console when the oldest message in the queue has been living in there for more that 60 seconds 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/MessageAgeMonitor.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.util.GregorianCalendar;

import org.apache.log4j.Logger;

import com.ibm.mq.MQC;
import com.ibm.mq.MQEnvironment;
import com.ibm.mq.MQException;
import com.ibm.mq.MQGetMessageOptions;
import com.ibm.mq.MQMessage;
import com.ibm.mq.MQQueue;
import com.ibm.mq.MQQueueManager;

/**
 * Simple message age monitor program that uses IBM MQ Java to read PCF-format
 * messages from a queue.
 */
public class MessageAgeMonitor implements Runnable {
 protected static Logger logger = Logger.getLogger(MessageAgeMonitor.class);
 protected static boolean isDebugEnabled = logger.isDebugEnabled();
 
 final private String qmgrName;
 final private String host;
 final private int port;
 final private String queueName;
 final private int alertAge; // in seconds
 final private String channel;
 
 final static int Polling_Freq = 30 * 1000; // 30 seconds

 MessageAgeMonitor(String qmgrName, String host, int port, String queueName, String channel,
   int alertAge) {
  this.qmgrName = qmgrName;
  this.host = host;
  this.port = port;
  this.channel = channel;
  this.queueName = queueName;
  this.alertAge = alertAge;
 }
 
 public void run() {
  if (isDebugEnabled)
   logger.debug("Starting Message Age monitor for " + queueName + "...");
  while (true) {
   checkAge();
   try {
    Thread.sleep(Polling_Freq); // sleep for 30 seconds
   }
   catch (InterruptedException e) {
    logger.info("The monitor has been interrupted, exit...");
    break;
   }
  }
 }

 private void checkAge() {
  MQQueueManager qm = null;
  MQQueue queue = null;
  if (isDebugEnabled) {
   logger.debug("Connecting to " + qmgrName + " at " + host + ":" + port + " over " + channel);
  }
        try {
   // Turn off unnecessary output before we start
   MQEnvironment.disableTracing ();
         MQException.log = null;
   MQEnvironment.hostname = host;
   MQEnvironment.port = port;
   MQEnvironment.channel = channel;
   qm = new MQQueueManager ("");
        queue = qm.accessQueue (queueName, MQC.MQOO_BROWSE | MQC.MQOO_FAIL_IF_QUIESCING);
            MQMessage message = new MQMessage ();
            MQGetMessageOptions gmo = new MQGetMessageOptions ();
            
            gmo.options = MQC.MQGMO_BROWSE_FIRST | MQC.MQGMO_NO_WAIT | MQC.MQGMO_CONVERT;
         
         message.messageId = null;
         message.correlationId = null;
         queue.get (message, gmo);
         // get message put date time
         GregorianCalendar cal = message.putDateTime;
         long ageInMillis = new java.util.Date().getTime() - cal.getTime().getTime();
         int ageInSeconds = (int) ageInMillis/1000;
         if (isDebugEnabled)
    logger.debug("Put Date: " + cal.getTime() + " age in seconds: " + ageInSeconds);
   if (ageInSeconds > alertAge) {
    logger.info(qmgrName + "/" + queueName + " age = " + ageInSeconds
      + ", exceeded alert threshold: " + alertAge);
    // XXX: add your code here to send out alert
   }
        }
        catch (MQException mqe) {
         if (mqe.reasonCode == MQException.MQRC_NO_MSG_AVAILABLE) {
          if (isDebugEnabled) {
           logger.debug("Queue " + qmgrName + "/" + queueName + " is empty.");
          }
         }
         else {
          logger.error("MQException caught", mqe);
         }
        }
        finally {
         if (queue != null && queue.isOpen()) {
          try {
           queue.close();
          }
          catch (Exception e) {
     logger.error("Exception caught during queue.close()", e);
          }
         }
   if (qm != null) {
    if (qm.isOpen()) {
     try {
      qm.close();
     }
     catch (Exception e) {
      logger.error("Exception caught during qm.close()", e);
     }
    }
    if (qm.isConnected()) {
     try {
      qm.disconnect();
     }
     catch (Exception e) {
      logger.error("Exception caught during qm.disconnect()", e);
     }
    }
   }
        }
    }

 public static void main (String [] args) {
  String qmgrName = "QMGR";
  String host = "localhost";
  int port = 1450;
  String channel = "SYSTEM.DEF.SVRCONN";
  String queueName = "TEST_QUEUE";
  
  MessageAgeMonitor monitor = new MessageAgeMonitor(qmgrName, host, port, queueName, channel, 60);
  new Thread(monitor).start();
 }
}

Monday, November 30, 2009

Use wss4j with Axis1.4 for message encryption and signing

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

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

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

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

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

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

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

Next we will create a key pair for message signing:

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

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

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

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

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

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

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

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

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

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

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

Now the detailed steps:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Thursday, September 10, 2009

Build a reliable email reader - Part 1

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

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

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

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

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

import org.apache.log4j.Logger;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Build a reliable email reader - Part 2

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

import java.io.Serializable;

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

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

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

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

 public boolean isExchange() {
  return isExchange;
 }

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

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

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

import org.apache.log4j.Logger;

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

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

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

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

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

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

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

Friday, September 4, 2009

Simple IBM MQ Queue Depth Monitor

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

import java.io.IOException;

import org.apache.log4j.Logger;

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

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

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

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

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

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

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

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

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

Detect bounced emails - Part 3

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

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

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

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

import org.apache.log4j.Logger;

import blog.javaclue.javamail.SmtpScanner.BOUNCE_TYPES;

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

 private final SmtpScanner rfcScan;

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

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

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

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

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

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

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

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

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

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

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

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

  return bounceType;
 }

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

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

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

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

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

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

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

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

import org.apache.log4j.Logger;

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

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

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.