/**
* See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Board of Regents of the University of Wisconsin System
* 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:
*
* 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 com.microsoft.exchange.autodiscover;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.microsoft.exchange.exception.AutodiscoverException;
import com.microsoft.exchange.exception.PoxAutodiscoverException;
/**
* An autodiscover implementation that queries all potential POX
* autodiscover endpoints for a given email address
*
* @see <a
* href="http://msdn.microsoft.com/EN-US/library/office/ee332364(v=exchg.140).aspx">Implementing
* an Autodiscover Client in Microsoft Exchange</a>
*
* @author ctcudd
*
*/
public class PoxAutodiscoverServiceImpl extends AbstractExchangeAutodiscoverService{
/**
* client should be configured with PoolingClientConnectionManager and Autodiscovery Redirect Strategy
*/
private DefaultHttpClient httpClient;
protected final Log log = LogFactory.getLog(this.getClass());
private static final String ENDPOINT_SUFFIX = "xml";
@Override
public String getServiceSuffix(){
return ENDPOINT_SUFFIX;
}
private static final String POX_REQUEST_FORMAT = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006\">" +
"<Request>"+
"<EMailAddress>%s</EMailAddress>"+
"<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>"+
"</Request>"+
"</Autodiscover>";
private static final String AS_REQUEST_FORMAT = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006\">" +
"<Request>"+
"<EMailAddress>%s</EMailAddress>"+
"<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006</AcceptableResponseSchema>" +
"</Request>"+
"</Autodiscover>";
@Override
public String getAutodiscoverEndpoint(String email) throws AutodiscoverException {
String ewsUrl = null;
String responseString = null;
String payload = String.format(POX_REQUEST_FORMAT,email);
for(String potential : getPotentialAutodiscoverEndpoints(email)){
log.info("attempting pox autodiscover for email="+email+" uri="+potential);
HttpPost request = new HttpPost(potential);
StringEntity requestEntity = new StringEntity(payload, getContentType());
request.setEntity(requestEntity);
try {
HttpResponse response = executeInternal(request);
responseString = parseHttpResponseToString(response);
if(StringUtils.isNotBlank(responseString)){
ewsUrl = parseResponseString(responseString);
if(StringUtils.isNotBlank(ewsUrl))
return ewsUrl;
}
} catch (Exception e) {
log.warn("caught exception while attempting POX autodiscover: "+e.getMessage());
}
}
throw new PoxAutodiscoverException("POX autodiscover failed. cannot find ewsurl for email="+email);
}
private HttpResponse executeInternal(HttpPost request) {
HttpResponse response = null;
try {
response = getHttpClient().execute(request);
} catch (Exception e) {
log.error("Failed to execute request="+request+". "+e.getMessage());
}
return response;
}
/**
* Parses Autodiscover response {@see http://msdn.microsoft.com/en-us/library/office/bb204082(v=exchg.150).aspx}
* Looking for an EWS url.
*
*
* mostly from http://dev.dartmouth.edu/svn/softdev/email/exchange/exchangeweb/trunk/src/edu/dartmouth/protocol/autodiscover/pox/POXAutodiscover.java
*
* @param xmlResponseString
* @return
* @throws IOException
* @throws SAXException
* @throws ParserConfigurationException
*/
public String parseResponseString(String xmlResponseString) throws PoxAutodiscoverException, SAXException, IOException, ParserConfigurationException{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
InputSource is = new InputSource();
is.setCharacterStream(new StringReader(xmlResponseString));
Document doc = db.parse(is);
// Verify there's an Autodiscover element, if not, response is invalid
NodeList autodiscover = doc.getElementsByTagName("Autodiscover");
if(autodiscover.getLength() != 1) {
throw(new PoxAutodiscoverException("Autodiscover tag not found in response: " + xmlResponseString));
}
// Verify there's an Action element, if not, response is invalid
NodeList action = doc.getElementsByTagName("Action");
if(action.getLength() != 1) {
throw(new PoxAutodiscoverException("No Action nodes found in response: " + xmlResponseString));
}
// Check value of Action element for redirects
Element line = (Element) action.item(0);
String actionData = getCharacterDataFromElement(line);
if(actionData == null) {
throw(new PoxAutodiscoverException("Unable to read data from Action element: " + xmlResponseString));
}
// Redirect, a URL will be provided in RedirectUrl Element
if(actionData.toLowerCase().equals("redirecturl")) {
NodeList redirectUrl = doc.getElementsByTagName("RedirectUrl");
if(redirectUrl.getLength() != 1) {
throw(new PoxAutodiscoverException("Expected redirectUrl node not found in response: " + xmlResponseString));
}
line = (Element) redirectUrl.item(0);
String redirectUrlData = getCharacterDataFromElement(line);
if(redirectUrlData == null) {
throw(new PoxAutodiscoverException("Unable to read data from RedirectUrl element: " + xmlResponseString));
}
throw(new PoxAutodiscoverException("RedirectUrl = "+redirectUrlData));
// Redirect, a new mailbox be provided in RedirectAddr Element
} else if (actionData.toLowerCase().equals("redirectaddr")) {
NodeList redirectAddr = doc.getElementsByTagName("RedirectAddr");
if(redirectAddr.getLength() != 1) {
throw(new PoxAutodiscoverException("Expected redirectAddr node not found in response: " + xmlResponseString));
}
line = (Element) redirectAddr.item(0);
String redirectAddrData = getCharacterDataFromElement(line);
if(redirectAddrData == null) {
throw(new PoxAutodiscoverException("Unable to read data from RedirectAddr element: " + xmlResponseString));
}
throw(new PoxAutodiscoverException("RedirectAddr = "+redirectAddrData));
}
// Verify there's a Protocol element, if not, response is invalid
NodeList protocols = doc.getElementsByTagName("Protocol");
if(protocols.getLength() < 1) {
throw(new PoxAutodiscoverException("No protocol nodes found in response: " + xmlResponseString));
}
for (int i = 0; i < protocols.getLength(); i++) {
Element element = (Element) protocols.item(i);
NodeList type = element.getElementsByTagName("Type");
if(type.getLength() != 1) {
throw(new PoxAutodiscoverException("Expected Type node not found in response: " + xmlResponseString));
}
line = (Element) type.item(0);
String typeData = getCharacterDataFromElement(line);
if(typeData == null) {
throw(new PoxAutodiscoverException("Unable to read data from Type element: " + xmlResponseString));
}
// Look for Protocol type "EXCH"
if(typeData.toLowerCase().equals("exch")) {
NodeList server = element.getElementsByTagName("Server");
if(server.getLength() != 1) {
throw(new PoxAutodiscoverException("Expected Server node not found in EXCH Protocol node in response: " + xmlResponseString));
}
line = (Element) server.item(0);
String exchangeServer = getCharacterDataFromElement(line);
if(exchangeServer == null) {
throw(new PoxAutodiscoverException("Unable to read data from Server element in EXCH Protocol node: " + xmlResponseString));
}
NodeList ewsUrl = element.getElementsByTagName("EwsUrl");
if(ewsUrl.getLength() != 1) {
throw(new PoxAutodiscoverException("Expected EwsUrl node not found in EXCH Protocol node in response: " + xmlResponseString));
}
line = (Element) ewsUrl.item(0);
String exchangeEwsUrl = getCharacterDataFromElement(line);
if(exchangeEwsUrl == null) {
throw(new PoxAutodiscoverException("Unable to read data from EwsUrl element in EXCH Protocol node: " + xmlResponseString));
}
return exchangeEwsUrl;
}
}
// If we reach this point, no EXCH Protocol found in response
throw(new PoxAutodiscoverException("Expected EXCH Type Protocol node not found in response: " + xmlResponseString));
}
/**
* Static utility method to extract text from an XML element
* @param e - an xml element
* @return the character datafrom the first child element as a string
*/
private static String getCharacterDataFromElement(Element e) {
Node child = e.getFirstChild();
if (child instanceof CharacterData) {
CharacterData cd = (CharacterData) child;
return cd.getData();
}
return null;
}
/**
* Copies the content form an HTTP response to a String and consumes the response entity.
* @param response
* @return
*/
private String parseHttpResponseToString(HttpResponse response) {
String responseContent = "";
StatusLine statusLine = response.getStatusLine();
HttpEntity responseEntity = response.getEntity();
StringWriter outputStream = new StringWriter();
InputStream inputStream = null;
try {
inputStream = responseEntity.getContent();
IOUtils.copy(inputStream, outputStream);
responseContent = outputStream.toString();
log.trace("HttpResponse: StatusCode="+statusLine.getStatusCode()+",Message="+statusLine.getReasonPhrase()+",Response="+responseContent);
} catch (IOException e) {
log.warn("Failed to parse HttpResponse:" +response, e);
} finally {
quietlyConsume(responseEntity);
}
return responseContent;
}
private void quietlyConsume(HttpEntity entity) {
try {
EntityUtils.consume(entity);
} catch (IOException e) {
log.info("caught IOException from EntityUtils#consume", e);
}
}
public DefaultHttpClient getHttpClient() {
return httpClient;
}
@Autowired
public void setHttpClient(DefaultHttpClient httpClient) {
this.httpClient = httpClient;
}
}