Thursday, November 17, 2011

Creating an Outlook task in Java using webDAV

I've recently come across this interesting task. In Java, create MS Exchange tasks with assigned reminders and custom task body. I was told that PoC (proof of concept) had been successfully concluded and it was just a matter of integration. To be honest, the PoC came up with this idea - create a web application which would utilize jIntegra DCOM wrapper (as the web would be running in Linux so no COM there) and remotely access MS Outlook installed somewhere and from there it would create tasks in Exchange. As you can imagine, the level of complexity of this approach is enormous. This solution would do a round trip around the world just to access an Exchange server and create a task there. Still this solution would eventually fail because of Outlooks scripting protection (see it here) and would require someone permanently logged in Outlook so DCOM calls would be possible.
What other (if any) solutions are out there. One pretty straight forward and surprisingly Java friendly too. Long time ago someone invented this extension to HTTP called webDAV. Yes, it's still around - alive and kicking. As webDAV is generally speaking just a collection of additional HTTP verbs (methods) it can be easily invoked from any language as long as the program can access HTTP (i.e. all).
Below's how I did it. You only need to download Jackrabbit jar ( freely available from Apache) and run the code. It internally performs a webDAV call that creates a new Exchange task. Tested with Exchange 2003 SP2 but should work with any 2003+ version. Additional files can be found under the main one. It's just a small tweak to Jackrabbit's own PropPatch method (adding PropPatch call with the XML in a String) and HtmlEncode helper (not my invention!) class to allow html task body encoding.

More info and source can be found at these URLs:
package uk.co.mo.webDAV.utils;

import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;

import java.util.Date;


public class TaskCreator {

    public static void main(String[] args) throws Exception {

        // create a httpclient config
        String uri = "http://xxx.yyy.zzz/exchange/";
        // String uri = "http://xxx.yyy.zzz/exchange/";
        HostConfiguration hostConfig = new HostConfiguration();
        hostConfig.setHost(uri);

        HttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
        HttpConnectionManagerParams params = new HttpConnectionManagerParams();
        int maxHostConnections = 20;
        params.setMaxConnectionsPerHost(hostConfig, maxHostConnections);
        connectionManager.setParams(params);

        HttpClient client = new HttpClient(connectionManager);
        client.setHostConfiguration(hostConfig);

        // set authentication (most exchange servers accept nt challenge-response)
        Credentials creds = new NTCredentials("admin_username", "password", "ip", "domain");
        // Credentials creds = new NTCredentials("username", "password", "domain controller", "domain");
        client.getState().setCredentials(AuthScope.ANY, creds);

        PropPatchMethodPro pPatch = new uk.co.mo.webDAV.utils.PropPatchMethodPro("http://xxx.yyy.zzz/exchange/user/Tasks/nameOfThetask" + new Date().getTime() + ".eml", getMessage());
        // PropPatchMethodPro pPatch = new uk.co.mo.webDAV.utils.PropPatchMethodPro ("http://xxx.yyy.zzz/exchange/user/Tasks/nameOftheTask") new Date().getTime() + ".eml", getMessage());

        pPatch.setRequestHeader("Content-type", "text/xml; charset=UTF-8");

        // call exchange
        client.executeMethod(pPatch);

        // is everything OK
        if (!pPatch.isSuccess(pPatch.getStatusCode())) {
            // we've got a problem
            System.out.println("PROBLEM");
            System.out.println(pPatch.getStatusCode());
            System.out.println(pPatch.getStatusText());
            System.out.println(pPatch.getStatusLine());
        } else {
            // processed
            System.out.println("OK");
        }
    }

    private static String getMessage() {
        return new StringBuilder("<?xml version=\"1.0\"?>\n")
                .append("<g:propertyupdate xmlns:g=\"DAV:\"\n")
                .append("xmlns:e=\"http://schemas.microsoft.com/exchange/\"\n")
                .append("xmlns:mapi=\"http://schemas.microsoft.com/mapi/\"\n")
                .append("xmlns:mapit=\"http://schemas.microsoft.com/mapi/proptag/\"\n")
                .append("xmlns:dt=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\"\n")
                .append("xmlns:h=\"http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/\"\n")
                .append("xmlns:i=\"http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-C000-000000000046}/\"\n")
                .append("xmlns:header=\"urn:schemas:mailheader:\"\n")
                .append("xmlns:sql=\"urn:schemas-microsoft-com:mapping-schema\"\n")
                .append("xmlns:mail=\"urn:schemas:httpmail:\"\n")
                .append("xmlns:t=\"http://schemas.microsoft.com/exchange/tasks/\">\n")
                .append("<g:set>\n")
                .append("<g:prop>\n")
                .append("<g:contentclass>urn:content-classes:task</g:contentclass>\n")
                .append("<e:outlookmessageclass>IPM.Task</e:outlookmessageclass>\n") // we're creating a task
                .append("<mail:subject>Test Task</mail:subject>\n") //subject line of the task
                .append("<mail:htmldescription>") // html message - needs to html encoded
                .append(HtmlEncode.text("Task Test<br/><b>test bold</b><pre>test pre</pre><a href='www.google.com'>www.google.com</a>"))
                .append("</mail:htmldescription>\n")
                .append("<h:0x8104 dt:dt=\"dateTime.tz\">2011-11-20T01:00:00.00Z</h:0x8104>\n") //start date
                .append("<h:0x8105 dt:dt=\"dateTime.tz\">2011-11-21T01:00:00.00Z</h:0x8105>\n") //due date
                .append("<i:0x8516 dt:dt=\"dateTime.tz\">2011-11-20T01:00:00.00Z</i:0x8516>\n") //start date
                .append("<i:0x8517 dt:dt=\"dateTime.tz\">2011-11-21T01:00:00.00Z</i:0x8517>\n") //due date
                .append("<i:0x8502 dt:dt=\"dateTime.tz\">2011-11-20T15:50:00.00Z</i:0x8502>\n") // reminder date
                .append("<h:0x811C dt:dt=\"boolean\">0</h:0x811C>\n")  // completed
                .append("<h:0x8101 dt:dt=\"int\">0</h:0x8101>\n") // status
                .append("<h:0x8102 dt:dt=\"float\">0</h:0x8102>\n") //completed %
                .append("<i:0x8503 dt:dt=\"boolean\">1</i:0x8503>\n")  // reminder enabled
                .append("<header:to>apollosystem</header:to>\n") // task destination
                .append("</g:prop>\n")
                .append("</g:set>\n")
                .append("</g:propertyupdate>").toString();
    }
}
package uk.co.mo.webDAV.utils;

import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.jackrabbit.webdav.DavConstants;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.client.methods.DavMethodBase;
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DavPropertyNameIterator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.MultiStatus;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.Status;
import org.apache.jackrabbit.webdav.xml.DomUtil;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.io.IOException;

public class PropPatchMethodPro extends DavMethodBase implements DavConstants {
    private static Logger log = LoggerFactory.getLogger(PropPatchMethodPro.class);

    private final DavPropertyNameSet propertyNames = new DavPropertyNameSet();

    private DavException responseException;

    /**
     * @param uri
     * @throws IOException
     */
    public PropPatchMethodPro(String uri, Document document) throws IOException {
        super(uri);
        setRequestBody(document);
    }

    public PropPatchMethodPro(String uri, String document) throws IOException {
        super(uri);
        setRequestBodyText(document);
    }

    public void setRequestBodyText(String requestBody) {
        setRequestEntity(new StringRequestEntity(requestBody));
    }


    private Element getPropElement(Element propUpdate, boolean isSet) {
        Element updateEntry = DomUtil.addChildElement(propUpdate, (isSet) ? XML_SET : XML_REMOVE, NAMESPACE);
        return DomUtil.addChildElement(updateEntry, XML_PROP, NAMESPACE);
    }

    //---------------------------------------------------------< HttpMethod >---

    /**
     * @see org.apache.commons.httpclient.HttpMethod#getName()
     */
    @Override
    public String getName() {
        return DavMethods.METHOD_PROPPATCH;
    }

    //------------------------------------------------------< DavMethodBase >---

    /**
     * @param statusCode
     * @return true if status code is {@link DavServletResponse#SC_MULTI_STATUS 207 (Multi-Status)}.
     *         For compliance reason {@link DavServletResponse#SC_OK 200 (OK)} is
     *         interpreted as successful response as well.
     */
    @Override
    protected boolean isSuccess(int statusCode) {
        return statusCode == DavServletResponse.SC_MULTI_STATUS || statusCode == DavServletResponse.SC_OK;
    }

    /**
     * @param multiStatus
     * @param httpState
     * @param httpConnection
     */
    @Override
    protected void processMultiStatusBody(MultiStatus multiStatus, HttpState httpState, HttpConnection httpConnection) {
        // check of OK response contains all set/remove properties
        MultiStatusResponse[] resp = multiStatus.getResponses();
        if (resp.length != 1) {
            log.warn("Expected a single multi-status response in PROPPATCH.");
        }
        boolean success = true;
        // only check the first ms-response
        for (int i = 0; i < 1; i++) {
            DavPropertyNameSet okSet = resp[i].getPropertyNames(DavServletResponse.SC_OK);
            if (okSet.isEmpty()) {
                log.debug("PROPPATCH failed: No 'OK' response found for resource " + resp[i].getHref());
                success = false;
            } else {
                DavPropertyNameIterator it = propertyNames.iterator();
                while (it.hasNext()) {
                    DavPropertyName pn = it.nextPropertyName();
                    success = okSet.remove(pn);
                }
            }
            if (!okSet.isEmpty()) {
                StringBuffer b = new StringBuffer("The following properties outside of the original request where set or removed: ");
                DavPropertyNameIterator it = okSet.iterator();
                while (it.hasNext()) {
                    b.append(it.nextPropertyName().toString()).append("; ");
                }
                log.warn(b.toString());
            }
        }
        // if  build the error message
        if (!success) {
            Status[] st = resp[0].getStatus();
            // TODO: respect multiple error reasons (not only the first one)
            for (int i = 0; i < st.length && responseException == null; i++) {
                switch (st[i].getStatusCode()) {
                    case DavServletResponse.SC_FAILED_DEPENDENCY:
                        // ignore
                        break;
                    default:
                        responseException = new DavException(st[i].getStatusCode());
                }
            }
        }
    }

    /**
     * @return
     * @throws IOException
     */
    @Override
    public DavException getResponseException() throws IOException {
        checkUsed();
        if (getSuccess()) {
            String msg = "Cannot retrieve exception from successful response.";
            log.warn(msg);
            throw new IllegalStateException(msg);
        }
        if (responseException != null) {
            return responseException;
        } else {
            return super.getResponseException();
        }
    }
}
package uk.co.mo.webDAV.utils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
 * Encodes text and URL strings in various ways resulting HTML-safe text.
 * All methods are <code>null</code> safe.
 */
public class HtmlEncode {

    protected static String EMPTY_STRING = "";

    protected static final char[][] TEXT = new char[64][];
    protected static final char[][] BLOCK = new char[64][];
    protected static final char[][] URL = new char[256][];

    /**
     * Creates HTML lookup tables for faster encoding.
     */
    static {
        for (int i = 0; i < 64; i++) {
            TEXT[i] = new char[]{(char) i};
        }
        for (char c = 0; c < 256; c++) {
            try {
                URL[c] = URLEncoder.encode(String.valueOf(c),
                        "ISO-8859-1").toCharArray();
            } catch (UnsupportedEncodingException ueex) {
                ueex.printStackTrace();
            }
        }

        // special HTML characters
        TEXT['\''] = "'".toCharArray(); // apostrophe (''' doesn't work - it is not by the w3 specs)
        TEXT['"'] = """.toCharArray(); // double quote
        TEXT['&'] = "&".toCharArray(); // ampersand
        TEXT['<'] = "<".toCharArray(); // lower than
        TEXT['>'] = ">".toCharArray(); // greater than

        // text table
        System.arraycopy(TEXT, 0, BLOCK, 0, 64);
        BLOCK['\n'] = "<br>".toCharArray(); // ascii 10, new line
        BLOCK['\r'] = "<br>".toCharArray(); // ascii 13, carriage return
    }

    // ---------------------------------------------------------------- encode

    public static String text(Object object) {
        if (object == null) {
            return EMPTY_STRING;
        }
        return text(object.toString());
    }

    /**
     * Encodes a string to HTML-safe text. The following characters are replaced:
     * <ul>
     * <li>' with &#039; (&apos; doesn't work)</li>
     * <li>" with &quot;</li>
     * <li>& with &amp;</li>
     * <li>< with &lt;</li>
     * <li>> with &gt;</li>
     * </ul>
     *
     * @see #block(String)
     */
    public static String text(String text) {
        int len;
        if ((text == null) || ((len = text.length()) == 0)) {
            return EMPTY_STRING;
        }
        StringBuilder buffer = new StringBuilder(len + (len >> 2));
        for (int i = 0; i < len; i++) {
            char c = text.charAt(i);
            if (c < 64) {
                buffer.append(TEXT[c]);
            } else {
                buffer.append(c);
            }
        }
        return buffer.toString();
    }

    // ---------------------------------------------------------------- enocde text

    public static String block(Object object) {
        if (object == null) {
            return EMPTY_STRING;
        }
        return block(object.toString());
    }

    /**
     * Encodes text into HTML-safe block preserving paragraphes. Besides the {@link #text(String) default
     * special characters} the following are replaced, too:
     * <ul>
     * <li>\n with <br></li>
     * <li>\r with <br></li>
     * </ul>
     * <p/>
     * <p/>
     * Method accepts any of CR, LF, or CR+LF as a line terminator.
     */
    public static String block(String text) {
        int len;
        if ((text == null) || ((len = text.length()) == 0)) {
            return EMPTY_STRING;
        }
        StringBuilder buffer = new StringBuilder(len + (len >> 2));
        char c, prev = 0;
        for (int i = 0; i < len; i++, prev = c) {
            c = text.charAt(i);
            if ((c == '\n') && (prev == '\r')) {
                continue; // previously '\r' (CR) was encoded, so skip '\n' (LF)
            }
            if (c < 64) {
                buffer.append(BLOCK[c]);
            } else {
                buffer.append(c);
            }
        }
        return buffer.toString();
    }

    // ---------------------------------------------------------------- encode text strict

    public static String strict(Object object) {
        if (object == null) {
            return EMPTY_STRING;
        }
        return strict(object.toString());
    }

    /**
     * Encodes text int HTML-safe block and preserves format using smart spaces.
     * Additionaly to {@link #block(String)}, the following characters are replaced:
     * <p/>
     * <ul>
     * <li>\n with <br></li>
     * <li>\r with <br></li>
     * </ul>
     * <p/>
     * This method preserves the format as much as possible, using the combination of
     * not-breakable and common spaces.
     */
    public static String strict(String text) {
        int len;
        if ((text == null) || ((len = text.length()) == 0)) {
            return EMPTY_STRING;
        }
        StringBuilder buffer = new StringBuilder(len + (len >> 2));
        char c, prev = 0;
        boolean prevSpace = false;
        for (int i = 0; i < len; i++, prev = c) {
            c = text.charAt(i);

            if (c == ' ') {
                if (prev != ' ') {
                    prevSpace = false;
                }
                if (prevSpace == false) {
                    buffer.append(' ');
                } else {
                    buffer.append(" ");
                }
                prevSpace = !prevSpace;
                continue;
            }
            if ((c == '\n') && (prev == '\r')) {
                continue; // previously '\r' (CR) was encoded, so skip '\n' (LF)
            }
            if (c < 64) {
                buffer.append(BLOCK[c]);
            } else {
                buffer.append(c);
            }
        }
        return buffer.toString();
    }
}
Enjoy while it lasts!

2 comments:

Hal said...

Have been digging around on Google for an hour or so, came across this article. It seems like there should be better integration of Java and Outlook, (To create tasks etc...) I am experimenting using Java to automate workflow. I have already figured out how to create and edit excel documents. The goal of the project being: use data from an excel document to create outlook tasks....

I will look into this, I'm not paying MoyoSoft $700 for their licenses...

Good article.

-Nate

Unknown said...

If time allows I could try to transform this into a maven project - that way it could be very easy to up and running with Outlook tasks.