com.buzzdavidson.spork.client.OWAClient.java Source code

Java tutorial

Introduction

Here is the source code for com.buzzdavidson.spork.client.OWAClient.java

Source

/*
 * <LICENSEHEADER/>
 *
 */
package com.buzzdavidson.spork.client;

import com.buzzdavidson.spork.util.OWAUtils;
import com.buzzdavidson.spork.util.OWAMessageFilter;
import com.buzzdavidson.spork.util.XmlUtils;
import com.buzzdavidson.spork.webdav.WebDavMethod;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.apache.commons.httpclient.HttpClient;
import static com.buzzdavidson.spork.constant.OWAConstants.*;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.codec.binary.Base64;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * Simple OWA Client implementation.
 *
 * NOTE that this is NOT a thread-safe implementation, as it stores state!
 *
 * @author steve
 */
@EqualsAndHashCode
@ToString(includeFieldNames = true, exclude = "owaRootUrl")
public class OWAClient {

    /**
     * FQDN of OWA host
     */
    @Setter
    @NonNull
    private String serverHost;
    /**
     * Path on host to OWA login page - view page source on login form for "action"
     * typically "exchweb/bin/auth/owaauth.dll"
     */
    @Setter
    @NonNull
    private String loginPath;
    /**
     * URI Scheme (http or https, defaults to https)
     */
    @Setter
    @NonNull
    private String scheme;
    /**
     * Base path to mailbox, typically just "exchange"
     */
    @Setter
    @NonNull
    private String docBasePath;
    /**
     * Host to use for NT domain authentication.
     * Defaults to "localhost" - probably best to leave this alone.
     */
    @Setter
    @NonNull
    private String authHost;
    /**
     * Authentication domain. NT logins are often referred to via [authDomain]\[userName]
     */
    @Setter
    @NonNull
    private String authDomain;
    /**
     * Is this a "public computer"?  Defaults to false.  Affects server-side session length
     * and timeout
     */
    @Setter
    @NonNull
    private boolean publicComputer = false;
    /**
     * Controls maximum number of items retrieved from server in one session
     */
    @Setter
    @NonNull
    private int itemFetchLimit;
    /**
     * if true, will mark each item read when it is retrieved
     */
    @Setter
    @NonNull
    private boolean markReadAfterFetch = true;

    /**
     * If true, will include diagnostic headers in messages
     */
    @Setter
    private boolean wantDiagnostics = false;

    // Dynamic items (configured via code)
    /**
     * Base uri for OWA server.  Defined at runtime from scheme + serverHost
     */
    private transient URI baseUri = null;
    /**
     * HTTP Client for communicating with WebDAV
     */
    private transient HttpClient client;
    /**
     * Full address to logged in user's inbox - determined at runtime after login
     * (OWA redirects to this page upon successful login)
     */
    private transient String inboxAddress = null;
    /**
     * Root URL for WebDAV.  Determined at runtime.
     */
    private transient String owaRootUrl = null;
    private transient final Log logger = LogFactory.getLog(this.getClass());

    /**
     * Default constructor
     */
    public OWAClient() {
        client = new HttpClient();
    }

    /**
     * Assembles the base uri from scheme + serverHost
     * @return base URI
     */
    private URI getBaseUri() {
        if (baseUri == null) {
            try {
                baseUri = new URI(scheme, serverHost, "/", null);
            } catch (URISyntaxException ex) {
                throw new RuntimeException("Unable to create base URI", ex);
            }
        }
        return baseUri;
    }

    /**
     * Retrieve string representation of base uri
     * @return base URI as string
     */
    private String getBaseUrl() {
        return getBaseUri().toString();
    }

    /**
     * Perform login to OWA server given configuration parameters and supplied user/password
     *
     * @param userName  user name for login
     * @param password password for login
     * @return true if successfully authenticated
     */
    public boolean login(String userName, String password) {
        /**
         * Set NT credentials on client
         */
        Credentials cred;
        cred = new NTCredentials(userName, password, authHost, authDomain);
        client.getState().setCredentials(new AuthScope(authHost, AuthScope.ANY_PORT), cred);

        String authUrl = getBaseUrl() + loginPath;
        boolean retval = false;
        logger.info(String.format("Logging in to OWA using username [%s] at URL [%s] ", userName, authUrl));
        PostMethod post = new PostMethod(authUrl);
        post.setRequestHeader(PARAM_CONTENT_TYPE, PostMethod.FORM_URL_ENCODED_CONTENT_TYPE);
        post.addParameter(PARAM_DEST, getBaseUrl() + docBasePath);
        post.addParameter(PARAM_UNAME, userName);
        post.addParameter(PARAM_PWD, password);
        post.addParameter(PARAM_FLAGS,
                publicComputer ? OWA_FLAG_VALUE_PUBLIC.toString() : OWA_FLAG_VALUE_PRIVATE.toString());

        int status = 0;
        try {
            status = client.executeMethod(post);
            if (logger.isDebugEnabled()) {
                logger.debug("Post returned status code " + status);
            }
        } catch (Exception ex) {
            logger.error("Got error posting to login url", ex);
        }

        if (status == HttpStatus.SC_OK) {
            // We shouldn't see this in normal operation... Evaluate whether this actually ever occurs.
            logger.info("Login succeeded (no redirect specified)");
            retval = true;
        } else if (status == HttpStatus.SC_MOVED_TEMPORARILY) {
            // We typically receive a redirect upon successful authentication; the target url is the user's inbox
            Header locationHeader = post.getResponseHeader(HEADER_LOCATION);
            if (locationHeader != null) {
                String mbxRoot = getMailboxRoot(locationHeader.getValue());
                if (mbxRoot == null || mbxRoot.length() < 1) {
                    logger.info("Login failed - Check configuration and credentials.");
                    retval = false;
                } else {
                    owaRootUrl = mbxRoot;
                    inboxAddress = owaRootUrl + "/" + FOLDER_NAME_INBOX; // TODO is this sufficient?
                    logger.info("Login succeeded, redirect specified (redirecting to " + owaRootUrl + ")");
                    retval = true;
                }
            }
        } else {
            logger.error("Login failed with error code " + status);
        }
        return retval;
    }

    /**
     * Determine the mailbox root by fetching the page at the supplied URL and examining
     * the subsequent page for the <Base tag - this value becomes the mailbox root.
     *
     * @param url URL to fetch page from
     * @return mailbox root or null if not found
     */
    private String getMailboxRoot(String url) {
        String retval = null;
        GetMethod get = new GetMethod(url);
        get.setFollowRedirects(true);
        get.setDoAuthentication(true);
        try {
            logger.info("Retrieving mailbox root via url " + url);
            int status = client.executeMethod(get);
            if (logger.isDebugEnabled()) {
                logger.info("Mailbox root retrieval returned status code " + status);
            }
            if (status == HttpStatus.SC_OK) {
                String line;
                BufferedReader bodyReader = new BufferedReader(
                        new InputStreamReader(get.getResponseBodyAsStream()));

                while ((line = bodyReader.readLine()) != null) {
                    int pos = (line.toUpperCase().indexOf(BASE_TAG));
                    if (pos >= 0) {
                        retval = snagMailboxRoot(line);
                        break;
                    }
                }
            }
        } catch (HttpException ex) {
            logger.error("Received HttpException retrieving mailbox root", ex);
        } catch (UnsupportedEncodingException ex) {
            logger.error("Received UnsupportedEncodingException retrieving mailbox root", ex);
        } catch (IOException ex) {
            logger.error("Received IOException retrieving mailbox root", ex);
        }
        if (retval != null && retval.length() > 0) {
            logger.info("Found mailbox root [" + retval + "]");
        }
        return retval;
    }

    /**
     * Retrieve mailbox root from supplied value.  Expects the line containing "<BASE" tag,
     * looks for a quoted string for the value.
     *
     * @param value value to parse
     * @return mailbox root
     */
    private String snagMailboxRoot(String value) {
        int i1 = value.indexOf('"');
        int i2 = value.lastIndexOf('"');
        if (i1 > 0 && i2 > 0 && i2 > i1) {
            return value.substring(i1 + 1, i2 - 1);
        } else {
            return null;
        }
    }

    /**
     * Retrieve list of messages via OWA/WebDAV
     *
     * @param filter type of list to retrieve (new or all messages)
     * @return list of mime messages (never null)
     */
    public List<MimeMessage> fetchMail(OWAMessageFilter filter) {
        List<MimeMessage> retval = new ArrayList<MimeMessage>();
        try {
            WebDavMethod search = new WebDavMethod(inboxAddress, OWA_METHOD_SEARCH, OWA_HEADERS_BRIEF);
            String msg = "";
            if (OWAMessageFilter.ALL_MESSAGES.equals(filter)) {
                msg = OWA_QUERY_GET_LIST_MAIL_MSG_ALL;
            } else {
                msg = OWA_QUERY_GET_LIST_MAIL_MSG;
            }
            logger.info(String.format("Requesting %s mail messages",
                    OWAMessageFilter.ALL_MESSAGES.equals(filter) ? "all" : "new"));
            search.setRequestEntity(new StringRequestEntity(msg, null, null));
            int status = client.executeMethod(search);
            if (logger.isDebugEnabled()) {
                logger.info("Message request returned status code [" + status + "]");
            }
            if (status == HttpStatus.SC_MULTI_STATUS) {
                Document doc = XmlUtils.readResponseDocument(search);
                Node email = XmlUtils.getChild(doc, OWA_FILTER_FIRST_MESSAGE_PATH);
                int msgCount;
                for (msgCount = 0; email != null; email = email.getNextSibling()) {
                    msgCount++;
                    if (msgCount > itemFetchLimit) {
                        break;
                    } else {
                        String nodeName = email.getNodeName();
                        if (nodeName.endsWith(OWA_MSG_RESPONSE_SUFFIX)) {
                            MimeMessage newMessage = OWAUtils.toMimeMessage(email);
                            if (newMessage != null) {
                                String messageUrl = OWAUtils.getMessageURL(email);
                                populateMessage(newMessage, messageUrl);
                                if (markReadAfterFetch) {
                                    markMessageRead(messageUrl);
                                }
                                retval.add(newMessage);
                            }
                        }
                    }
                }
            }
            return retval;
        } catch (HttpException ex) {
            logger.error("Received HttpException fetching mail", ex);
        } catch (UnsupportedEncodingException ex) {
            logger.error("Received UnsupportedEncodingException fetching mail", ex);
        } catch (IOException ex) {
            logger.error("Received IOException fetching mail", ex);
        }
        return retval;
    }

    public void markMessageRead(String messageUrl) {
        String filename = messageUrl.substring(messageUrl.lastIndexOf("/") + 1);
        String query = String.format(OWA_QUERY_MARK_AS_READ, filename);
        WebDavMethod setRead = new WebDavMethod(inboxAddress + "/", OWA_METHOD_PROP_PATCH, OWA_HEADERS_MATCH);
        try {
            setRead.setRequestEntity(new StringRequestEntity(query, null, null));
            logger.info("Marking message as read [" + filename + "]");
            int status = client.executeMethod(setRead);
            if (logger.isDebugEnabled()) {
                logger.debug("Message request returned status code [" + status + "]");
            }
        } catch (HttpException ex) {
            logger.error("Received HttpException fetching mail", ex);
        } catch (UnsupportedEncodingException ex) {
            logger.error("Received UnsupportedEncodingException fetching mail", ex);
        } catch (IOException ex) {
            logger.error("Received IOException fetching mail", ex);
        }
    }

    public boolean removeMessage(String messageUrl) {
        boolean retval = false;
        String filename = messageUrl.substring(messageUrl.lastIndexOf("/") + 1);
        String query = String.format(OWA_QUERY_DELETE_MESSAGE, filename);
        WebDavMethod del = new WebDavMethod(inboxAddress + "/", OWA_METHOD_DELETE, OWA_HEADERS_MATCH);
        try {
            del.setRequestEntity(new StringRequestEntity(query, null, null));
            logger.info("Deleting message [" + filename + "]");
            int status = client.executeMethod(del);
            retval = (status == HttpStatus.SC_OK);
            if (logger.isDebugEnabled()) {
                logger.debug("Message request returned status code [" + status + "]");
            }
        } catch (HttpException ex) {
            logger.error("Received HttpException fetching mail", ex);
        } catch (UnsupportedEncodingException ex) {
            logger.error("Received UnsupportedEncodingException fetching mail", ex);
        } catch (IOException ex) {
            logger.error("Received IOException fetching mail", ex);
        }
        return retval;
    }

    /**
     * Fetch the body of the email message given a message URL.
     *
     * @param message Mime message to add body to
     * @param messageUrl URL of message (OWA WebDav)
     */
    private void populateMessage(MimeMessage message, String messageUrl) {
        GetMethod get = new GetMethod(messageUrl);
        get.setRequestHeader("Translate", "F");
        try {
            logger.info("Requesting message body via url [" + messageUrl + "]");
            int status = client.executeMethod(get);
            if (logger.isDebugEnabled()) {
                logger.debug("Message request returned status code [" + status + "]");
            }
            if (status == HttpStatus.SC_OK) {
                BufferedReader in = new BufferedReader(new InputStreamReader(get.getResponseBodyAsStream()));
                String contentType = OWAUtils.getSingleHeader(message, PARAM_CONTENT_TYPE);
                String contentEncoding = OWAUtils.getSingleHeader(message, PARAM_CONTENT_ENCODING);
                boolean needContentType = (contentType == null || contentType.length() < 1);

                // first set of entries are headers, followed by empty line.  skip to first empty line.
                for (String line; (line = in.readLine()) != null;) {
                    if (line != null && line.trim().length() == 0) {
                        break;
                    }
                }

                StringBuffer buf = new StringBuffer();
                for (String line; (line = in.readLine()) != null;) {
                    buf.append(line);
                    buf.append(NEWLINE);
                }

                if (needContentType || contentType.contains(CONTENT_TYPE_TEXT_PLAIN)) {
                    // OWA sometimes reports XML or HTML content as "text/plain"; check for this.

                    if (logger.isDebugEnabled()) {
                        logger.debug("checking content encoding");
                    }
                    byte[] blob = buf.toString().getBytes();
                    if (logger.isDebugEnabled()) {
                        logger.debug("blob length is " + blob.length + " bytes");
                    }
                    if (contentEncoding.equals(BASE_64_ENCODING)) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("blob is base64 encoded, decoding");
                        }
                        byte[] newblob = Base64.decodeBase64(blob);
                        if (logger.isDebugEnabled()) {
                            logger.debug("decoded blob length is " + newblob.length + " bytes");
                        }
                        String data = new String(newblob);
                        buf = new StringBuffer(data);
                    } else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("blob is encoded with [" + contentEncoding + "], not decoding");
                        }
                    }
                    contentType = determineContentType(buf, message);
                }

                if (wantDiagnostics) {
                    buf.append(NEWLINE);
                    buf.append("-------------------------------------");
                    buf.append(NEWLINE);
                    buf.append("SPORK Data");
                    buf.append(NEWLINE);
                    buf.append("content-encoding: " + contentEncoding);
                    buf.append(NEWLINE);
                    buf.append("content-type: " + contentType);
                    if (needContentType) {
                        buf.append(" (calculated) " + contentType);
                        buf.append(" Message processed "
                                + DateFormat.getDateInstance(DateFormat.SHORT).format(new Date()));
                    }
                }

                if (needContentType) {
                    buf = cleanupBuffer(buf);
                }

                buf.append(NEWLINE);
                message.setText(buf.toString());
                int len = buf.length();
                message.setHeader(PARAM_CONTENT_LENGTH, Integer.valueOf(len).toString()); // close enough :)
                if (contentType != null && contentType.length() > 0) {
                    message.setHeader(PARAM_CONTENT_TYPE, contentType);
                }
                message.saveChanges();
            }
        } catch (HttpException ex) {
            logger.error("Received HttpException fetching message body", ex);
        } catch (UnsupportedEncodingException ex) {
            logger.error("Received UnsupportedEncodingException fetching message body", ex);
        } catch (IOException ex) {
            logger.error("Received IOException fetching message body", ex);
        } catch (MessagingException ex) {
            logger.error("Received MessagingException fetching message body", ex);
        }
    }

    private String determineContentType(StringBuffer buffer, MimeMessage message) {
        if (buffer.indexOf("_=_NextPart_") >= 0) {
            return CONTENT_TYPE_MULTIPART;
        }
        if (buffer.indexOf("This is a multi-part message in MIME format") >= 0) {
            return CONTENT_TYPE_MULTIPART;
        }
        if (buffer.indexOf("<html") >= 0 || buffer.indexOf("<HTML") >= 0 || buffer.indexOf("<!DOCTYPE HTML") >= 0) {
            return CONTENT_TYPE_HTML + "; charset=UTF-8";
        }
        if (buffer.indexOf("<?xml") >= 0) {
            return CONTENT_TYPE_XML + "; charset=UTF-8";
        }
        return CONTENT_TYPE_DEFAULT;
    }

    private StringBuffer cleanupBuffer(StringBuffer buffer) {
        // we seem to get odd characters in messages with no specific content-type
        // TODO: how can we handle these properly?
        String[] STRIP_SEQ = { "=20=", "=20", "=0A=", "=0A", "=3D", " = ", " = ", "=" };
        String outbuf = buffer.toString();
        for (String seq : STRIP_SEQ) {
            outbuf = outbuf.replaceAll(seq, "");
        }
        return new StringBuffer(outbuf);
    }

}