/**
* 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:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;
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;
/**
* 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),
*
* <p>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).
*
* <p>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 static final String AUTODISCOVER_SCHEMA =
"http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006";
protected static final String AUTODISCOVER_RESPONSE_SCHEMA =
"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
protected static final QName REQUEST_SERVER_VERSION_QNAME =
new QName(
"http://schemas.microsoft.com/exchange/2010/Autodiscover", "RequestedServerVersion", "a");
protected static final QName SOAP_ACTION_HEADER_QNAME =
new QName("http://www.w3.org/2005/08/addressing", "Action", "wsa");
protected static final String SOAP_ACTION_BASE =
"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/";
protected static final String GET_USER_SETTINGS_ACTION = SOAP_ACTION_BASE + "GetUserSettings";
private static final String INTERNAL_EWS_SERVER = "InternalEwsUrl";
private static final 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;
}
}