org.jasig.portlet.emailpreview.dao.exchange.ExchangeAutoDiscoverDaoImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.portlet.emailpreview.dao.exchange.ExchangeAutoDiscoverDaoImpl.java

Source

/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.jasig.portlet.emailpreview.dao.exchange;

import com.microsoft.exchange.autodiscover.*;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.jasig.portlet.emailpreview.EmailPreviewException;
import org.jasig.portlet.emailpreview.MailStoreConfiguration;
import org.jasig.portlet.emailpreview.caching.EWSEndpointUriCacheKeyGeneratorImpl;
import org.jasig.portlet.emailpreview.caching.IEWSEndpoingUriCacheKeyGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.oxm.Marshaller;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.core.WebServiceMessageCallback;
import org.springframework.ws.client.core.WebServiceOperations;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.client.core.SoapActionCallback;
import org.springframework.xml.transform.StringResult;

import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Data accessor that uses Exchange Web Services's autodiscovery process.  See
 * http://msdn.microsoft.com/en-us/library/exchange/jj900169%28v=exchg.150%29.aspx (SOAP, spec on steps for autodiscover),
 *
 * Also see:
 * http://msdn.microsoft.com/en-us/library/exchange/dd877090%28v=exchg.150%29.aspx
 * http://msdn.microsoft.com/en-us/library/exchange/gg591268%28v=exchg.140%29.aspx (Exchange Online),
 * http://msdn.microsoft.com/en-us/library/ee332364.aspx (this is for the Plain-old XML approach, not implemented).
 *
 * The current implementation:
 * - Supports only the SOAP protocol, not the Plain-old XML approach
 * - Does not use the Active Directory Service Connection Point (SCP) strategy to lookup Autodiscover URLs
 * - Does not look up records in DNS
 * - The servername to first contact is configured in the portlet preferences.
 *
 * @author James Wennmacher, jwennmacher@unicon.net
 */

public class ExchangeAutoDiscoverDaoImpl implements IExchangeAutoDiscoverDao {

    protected final static String AUTODISCOVER_SCHEMA = "http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006";
    protected final static String AUTODISCOVER_RESPONSE_SCHEMA = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
    protected final static QName REQUEST_SERVER_VERSION_QNAME = new QName(
            "http://schemas.microsoft.com/exchange/2010/Autodiscover", "RequestedServerVersion", "a");
    protected final static QName SOAP_ACTION_HEADER_QNAME = new QName("http://www.w3.org/2005/08/addressing",
            "Action", "wsa");

    protected final static String SOAP_ACTION_BASE = "http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/";
    protected final static String GET_USER_SETTINGS_ACTION = SOAP_ACTION_BASE + "GetUserSettings";
    private final static String INTERNAL_EWS_SERVER = "InternalEwsUrl";
    private final static String EXTERNAL_EWS_SERVER = "ExternalEwsUrl";

    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private String EWSServerURI = "https://{server}/EWS/exchange.asmx";
    private final ObjectFactory objectFactory = new ObjectFactory();
    private Marshaller marshaller;

    @Autowired
    @Qualifier("mailboxServernameCache")
    private Cache ewsEndpointUriCache;
    private IEWSEndpoingUriCacheKeyGenerator ewsEndpointUriCacheKeyGenerator = new EWSEndpointUriCacheKeyGeneratorImpl();

    @Autowired
    @Qualifier(value = "exchangeAutodiscover")
    private WebServiceOperations webServiceOperations;

    // From http://msdn.microsoft.com/en-us/library/exchange/jj900169%28v=exchg.150%29.aspx
    private List<String> autoDiscoverURIs = Arrays
            .asList(new String[] { "https://{server}/autodiscover/autodiscover.svc",
                    "https://autodiscover.{server}/autodiscover/autodiscover.svc" });

    public void setWebServiceOperations(WebServiceOperations webServiceOperations) {
        this.webServiceOperations = webServiceOperations;
    }

    public void setEwsEndpointUriCache(Cache ewsEndpointUriCache) {
        this.ewsEndpointUriCache = ewsEndpointUriCache;
    }

    public void setEwsEndpointUriCacheKeyGenerator(
            IEWSEndpoingUriCacheKeyGenerator ewsEndpointUriCacheKeyGenerator) {
        this.ewsEndpointUriCacheKeyGenerator = ewsEndpointUriCacheKeyGenerator;
    }

    public void setAutoDiscoverURIs(List<String> autoDiscoverURIs) {
        this.autoDiscoverURIs = autoDiscoverURIs;
    }

    public void setEWSServerURI(String EWSServerURI) {
        this.EWSServerURI = EWSServerURI;
    }

    public void setMarshaller(Marshaller marshaller) {
        this.marshaller = marshaller;
    }

    // ----------------------------------------------------------
    // get Exchange Server for mailbox
    // ----------------------------------------------------------

    @Override
    public String getEndpointUri(MailStoreConfiguration config) {
        String cacheKey = ewsEndpointUriCacheKeyGenerator.getKey(config);
        Element element = ewsEndpointUriCache.get(cacheKey);
        if (element == null) {
            String endpointUri;
            if (config.isExchangeAutodiscover()) {
                log.debug(
                        "Cache miss. Autodiscover enabled. Looking up EWS endpoint for mail account {} at host {}",
                        config.getMailAccount(), config.getHost());
                endpointUri = getAccountServiceUrl(config);
                log.info("Caching Autodiscover endpoint {} for mail account {}", endpointUri,
                        config.getMailAccount());
            } else {
                endpointUri = EWSServerURI.replace("{server}", config.getHost());
                log.debug(
                        "Cache miss. Not using Autodiscover. Caching computed EWS endpoint {} for mail account {}",
                        endpointUri, config.getMailAccount());
            }
            Element el = new Element(cacheKey, endpointUri);
            ewsEndpointUriCache.put(el);
            return endpointUri;
        } else {
            String uri = (String) element.getValue();
            log.debug("Cache hit for EWS endpoint for mail account {} to endpoint URI {}", config.getMailAccount(),
                    uri);
            return uri;
        }
    }

    private String getAccountServiceUrl(MailStoreConfiguration config) {
        UserSettings userSettings = sendMessageAndExtractSingleResponse(
                createGetUserSettingsSoapMessage(config.getMailAccount()), GET_USER_SETTINGS_ACTION, config);

        //Give preference to Internal URL over External URL
        String internalUri = null;
        String externalUri = null;

        for (UserSetting userSetting : userSettings.getUserSettings()) {
            String potentialAccountServiceUrl = ((StringSetting) userSetting).getValue().getValue();
            if (EXTERNAL_EWS_SERVER.equals(userSetting.getName())) {
                externalUri = potentialAccountServiceUrl;
            }
            if (INTERNAL_EWS_SERVER.equals(userSetting.getName())) {
                internalUri = potentialAccountServiceUrl;
            }
        }
        if (internalUri == null && externalUri == null) {
            throw new EmailPreviewException("Unable to find EWS Server URI in properies " + EXTERNAL_EWS_SERVER
                    + " or " + INTERNAL_EWS_SERVER + " from User's Autodiscover record");
        }
        return internalUri != null ? internalUri : externalUri;
    }

    private GetUserSettingsRequestMessage createGetUserSettingsSoapMessage(String emailAddress) {
        GetUserSettingsRequest msg = objectFactory.createGetUserSettingsRequest();

        User user = objectFactory.createUser();
        user.setMailbox(emailAddress);
        Users users = objectFactory.createUsers();
        users.getUsers().add(user);
        msg.setUsers(users);

        msg.setRequestedVersion(ExchangeVersion.EXCHANGE_2010);

        RequestedSettings settings = objectFactory.createRequestedSettings();
        settings.getSettings().add(EXTERNAL_EWS_SERVER);
        settings.getSettings().add(INTERNAL_EWS_SERVER);
        msg.setRequestedSettings(settings);

        // Construct the SOAP request object to use
        GetUserSettingsRequestMessage request = objectFactory.createGetUserSettingsRequestMessage();
        request.setRequest(objectFactory.createGetUserSettingsRequestMessageRequest(msg));
        return request;
    }

    private UserSettings sendMessageAndExtractSingleResponse(GetUserSettingsRequestMessage soapRequest,
            String soapAction, MailStoreConfiguration config) {
        GetUserSettingsResponseMessage soapResponseMessage = (GetUserSettingsResponseMessage) sendSoapRequest(
                soapRequest, soapAction, config);
        GetUserSettingsResponse soapResponse = soapResponseMessage.getResponse().getValue();

        UserSettings userSettings = null;
        boolean warning = false;
        boolean error = false;
        StringBuilder msg = new StringBuilder();
        if (!ErrorCode.NO_ERROR.equals(soapResponse.getErrorCode())) {
            error = true;
            msg.append("Error: ").append(soapResponse.getErrorCode().value()).append(": ")
                    .append(soapResponse.getErrorMessage().getValue()).append("\n");
        } else {
            JAXBElement<ArrayOfUserResponse> JAXBresponseArray = soapResponse.getUserResponses();
            ArrayOfUserResponse responseArray = JAXBresponseArray != null ? JAXBresponseArray.getValue() : null;
            List<UserResponse> responses = responseArray != null ? responseArray.getUserResponses()
                    : new ArrayList<UserResponse>();
            if (responses.size() == 0) {
                error = true;
                msg.append("Error: Autodiscovery returned no Exchange mail server for mailbox");
            } else if (responses.size() > 1) {
                warning = true;
                msg.append("Warning: Autodiscovery returned multiple responses for Exchange server mailbox query");
            } else {
                UserResponse userResponse = responses.get(0);
                if (!ErrorCode.NO_ERROR.equals(userResponse.getErrorCode())) {
                    error = true;
                    msg.append("Received error message obtaining user mailbox's server. Error "
                            + userResponse.getErrorCode().value() + ": "
                            + userResponse.getErrorMessage().getValue());
                }
                userSettings = userResponse.getUserSettings().getValue();
            }
        }
        if (warning || error) {
            StringBuilder errorMessage = new StringBuilder("Unexpected response from soap action: " + soapAction
                    + ".\nSoap Request: " + soapRequest.toString() + "\n");
            errorMessage.append(msg);
            if (error) {
                throw new EmailPreviewException("Error performing Exchange web service action " + soapAction
                        + ". Error code is " + errorMessage.toString());
            }
            log.warn("Received warning response to soap request " + soapAction + ". Error text is:\n"
                    + errorMessage.toString());
            throw new EmailPreviewException("Unable to perform " + soapAction
                    + " operation; try again later. Message text: " + errorMessage.toString());
        }
        return userSettings;
    }

    // ----------------------------------------------------------
    // common send message and parse response
    // ----------------------------------------------------------

    private Object sendSoapRequest(Object soapRequest, String soapAction, MailStoreConfiguration config) {
        Exception ex = null;

        // Try each connection URI pattern until one works
        for (String pattern : autoDiscoverURIs) {
            String uri = pattern.replace("{server}", config.getHost());
            try {
                Object response = sendSoapMessageToServer(soapRequest, soapAction, uri);
                return response;
            } catch (WebServiceClientException e) {
                ex = e;
                // todo should we bother catching/wrapping? I think runtime exceptions should be caught at service layer
                throw new EmailPreviewException(e);
            }
            //todo figure out if we can catch authentication exceptions to return them separate. Useful?
        }
        throw new EmailPreviewException("Unable to use autodiscover on host " + config.getHost(), ex);
    }

    private Object sendSoapMessageToServer(Object soapRequest, final String soapAction, String uri) {
        final WebServiceMessageCallback actionCallback = new SoapActionCallback(soapAction);

        final WebServiceMessageCallback customCallback = new WebServiceMessageCallback() {

            @Override
            public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException {
                actionCallback.doWithMessage(message);
                SoapMessage soap = (SoapMessage) message;
                soap.getEnvelope().getHeader().addHeaderElement(REQUEST_SERVER_VERSION_QNAME)
                        .setText(ExchangeVersion.EXCHANGE_2010.value());
                soap.getEnvelope().getHeader().addHeaderElement(SOAP_ACTION_HEADER_QNAME).setText(soapAction);
            }

        };

        if (log.isDebugEnabled()) {
            StringResult message = new StringResult();
            try {
                marshaller.marshal(soapRequest, message);
                log.debug(
                        "Attempting to send SOAP request to {}\nSoap Action: {}\nSoap message body"
                                + " (not exact, log org.apache.http.wire to see actual message):\n{}",
                        uri, soapAction, message);
            } catch (IOException ex) {
                log.debug("IOException attempting to display soap response", ex);
            }
        }

        // use the request to retrieve data from the Exchange server
        Object response = webServiceOperations.marshalSendAndReceive(uri, soapRequest, customCallback);

        if (log.isDebugEnabled()) {
            StringResult messageResponse = new StringResult();
            try {
                marshaller.marshal(response, messageResponse);
                log.debug("Soap response body (not exact, log org.apache.http.wire to see actual message):\n{}",
                        messageResponse);
            } catch (IOException exception) {
                log.debug("IOException attempting to display soap response", exception);
            }
        }
        return response;
    }

}