diff --git a/src/java/davmail/exchange/dav/ExchangeDavMethod.java b/src/java/davmail/exchange/dav/ExchangeDavMethod.java new file mode 100644 index 00000000..8b67837e --- /dev/null +++ b/src/java/davmail/exchange/dav/ExchangeDavMethod.java @@ -0,0 +1,242 @@ +/* + * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway + * Copyright (C) 2012 Mickael Guessant + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package davmail.exchange.dav; + +import davmail.exchange.XMLStreamUtil; +import org.apache.commons.httpclient.*; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.RequestEntity; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.log4j.Logger; + +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * New stax based implementation to replace DOM based jackrabbit version an support Exchange only extensions. + */ +public abstract class ExchangeDavMethod extends PostMethod { + protected static final Logger LOGGER = Logger.getLogger(ExchangeDavMethod.class); + List responses; + + /** + * Create PROPPATCH method. + * + * @param path path + */ + public ExchangeDavMethod(String path) { + super(path); + setRequestEntity(new RequestEntity() { + byte[] content; + + public boolean isRepeatable() { + return true; + } + + public void writeRequest(OutputStream outputStream) throws IOException { + if (content == null) { + content = generateRequestContent(); + } + outputStream.write(content); + } + + public long getContentLength() { + if (content == null) { + content = generateRequestContent(); + } + return content.length; + } + + public String getContentType() { + return "text/xml;charset=UTF-8"; + } + }); + } + + /** + * Generate request content from property values. + * + * @return request content as byte array + */ + protected abstract byte[] generateRequestContent(); + + @Override + protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) { + Header contentTypeHeader = getResponseHeader("Content-Type"); + if (contentTypeHeader != null && "text/xml".equals(contentTypeHeader.getValue())) { + responses = new ArrayList(); + XMLStreamReader reader; + try { + reader = XMLStreamUtil.createXMLStreamReader(new FilterInputStream(getResponseBodyAsStream()) { + final byte[] lastbytes = new byte[3]; + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + int count = in.read(bytes, off, len); + // patch invalid element name + for (int i = 0; i < count; i++) { + byte currentByte = bytes[off + i]; + if ((lastbytes[0] == '<') && (currentByte >= '0' && currentByte <= '9')) { + // move invalid first tag char to valid range + bytes[off + i] = (byte) (currentByte + 49); + } + lastbytes[0] = lastbytes[1]; + lastbytes[1] = lastbytes[2]; + lastbytes[2] = currentByte; + } + return count; + } + + }); + while (reader.hasNext()) { + reader.next(); + if (XMLStreamUtil.isStartTag(reader, "response")) { + handleResponse(reader); + } + } + + } catch (IOException e) { + LOGGER.error("Error while parsing soap response: " + e, e); + } catch (XMLStreamException e) { + LOGGER.error("Error while parsing soap response: " + e, e); + } + } + } + + protected void handleResponse(XMLStreamReader reader) throws XMLStreamException { + String href = null; + String responseStatus = ""; + while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "response")) { + reader.next(); + if (XMLStreamUtil.isStartTag(reader)) { + String tagLocalName = reader.getLocalName(); + if ("href".equals(tagLocalName)) { + href = reader.getElementText(); + } else if ("status".equals(tagLocalName)) { + responseStatus = reader.getElementText(); + } else if ("propstat".equals(tagLocalName)) { + MultiStatusResponse multiStatusResponse = new MultiStatusResponse(href, responseStatus); + handlePropstat(reader, multiStatusResponse); + responses.add(multiStatusResponse); + } + } + } + + } + + protected void handlePropstat(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException { + int propstatStatus = 0; + while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "propstat")) { + reader.next(); + if (XMLStreamUtil.isStartTag(reader)) { + String tagLocalName = reader.getLocalName(); + if ("status".equals(tagLocalName)) { + if ("HTTP/1.1 200 OK".equals(reader.getElementText())) { + propstatStatus = HttpStatus.SC_OK; + } else { + propstatStatus = 0; + } + } else if ("prop".equals(tagLocalName) && propstatStatus == HttpStatus.SC_OK) { + handleProperty(reader, multiStatusResponse); + } + } + } + + } + + protected void handleProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException { + while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) { + reader.next(); + if (XMLStreamUtil.isStartTag(reader)) { + Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI()); + String tagLocalName = reader.getLocalName(); + String tagContent = getTagContent(reader); + if (tagContent != null) { + multiStatusResponse.add(new DefaultDavProperty(tagLocalName, tagContent, namespace)); + } + } + } + } + + protected String getTagContent(XMLStreamReader reader) throws XMLStreamException { + String value = null; + String tagLocalName = reader.getLocalName(); + while (reader.hasNext() && + !((reader.getEventType() == XMLStreamConstants.END_ELEMENT) && tagLocalName.equals(reader.getLocalName()))) { + reader.next(); + if (reader.getEventType() == XMLStreamConstants.CHARACTERS) { + value = reader.getText(); + } + } + // empty tag + if (!reader.hasNext()) { + throw new XMLStreamException("End element for " + tagLocalName + " not found"); + } + return value; + } + + /** + * Get Multistatus responses. + * + * @return responses + * @throws HttpException on error + */ + public MultiStatusResponse[] getResponses() throws HttpException { + if (responses == null) { + throw new HttpException(getStatusLine().toString()); + } + return responses.toArray(new MultiStatusResponse[responses.size()]); + } + + /** + * Get single Multistatus response. + * + * @return response + * @throws HttpException on error + */ + public MultiStatusResponse getResponse() throws HttpException { + if (responses == null || responses.size() != 1) { + throw new HttpException(getStatusLine().toString()); + } + return responses.get(0); + } + + /** + * Return method http status code. + * + * @return http status code + * @throws HttpException on error + */ + public int getResponseStatusCode() throws HttpException { + String responseDescription = getResponse().getResponseDescription(); + if ("HTTP/1.1 201 Created".equals(responseDescription)) { + return HttpStatus.SC_CREATED; + } else { + return HttpStatus.SC_OK; + } + } +} diff --git a/src/java/davmail/exchange/dav/ExchangePropFindMethod.java b/src/java/davmail/exchange/dav/ExchangePropFindMethod.java new file mode 100644 index 00000000..444e7738 --- /dev/null +++ b/src/java/davmail/exchange/dav/ExchangePropFindMethod.java @@ -0,0 +1,114 @@ +/* + * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway + * Copyright (C) 2011 Mickael Guessant + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package davmail.exchange.dav; + +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameIterator; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.log4j.Logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * Custom Exchange PROPFIND method. + * Does not load full DOM in memory. + */ +public class ExchangePropFindMethod extends ExchangeDavMethod { + protected static final Logger LOGGER = Logger.getLogger(ExchangePropFindMethod.class); + + protected DavPropertyNameSet propertyNameSet; + + public ExchangePropFindMethod(String uri) throws IOException { + this(uri, null, DepthHeader.DEPTH_INFINITY); + } + + public ExchangePropFindMethod(String uri, DavPropertyNameSet propertyNameSet, int depth) throws IOException { + super(uri); + this.propertyNameSet = propertyNameSet; + DepthHeader dh = new DepthHeader(depth); + setRequestHeader(dh.getHeaderName(), dh.getHeaderValue()); + } + + protected byte[] generateRequestContent() { + try { + // build namespace map + int currentChar = 'e'; + final Map nameSpaceMap = new HashMap(); + nameSpaceMap.put("DAV:", (int) 'D'); + if (propertyNameSet != null) { + DavPropertyNameIterator propertyNameIterator = propertyNameSet.iterator(); + while (propertyNameIterator.hasNext()) { + DavPropertyName davPropertyName = propertyNameIterator.nextPropertyName(); + + davPropertyName.getName(); + // property namespace + String namespaceUri = davPropertyName.getNamespace().getURI(); + if (!nameSpaceMap.containsKey(namespaceUri)) { + nameSpaceMap.put(namespaceUri, currentChar++); + } + } + } + // + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(baos, "UTF-8"); + writer.write(" mapEntry : nameSpaceMap.entrySet()) { + writer.write(" xmlns:"); + writer.write((char) mapEntry.getValue().intValue()); + writer.write("=\""); + writer.write(mapEntry.getKey()); + writer.write("\""); + } + writer.write(">"); + if (propertyNameSet == null || propertyNameSet.isEmpty()) { + writer.write(""); + } else { + writer.write(""); + DavPropertyNameIterator propertyNameIterator = propertyNameSet.iterator(); + while (propertyNameIterator.hasNext()) { + DavPropertyName davPropertyName = propertyNameIterator.nextPropertyName(); + char nameSpaceChar = (char) nameSpaceMap.get(davPropertyName.getNamespace().getURI()).intValue(); + writer.write('<'); + writer.write(nameSpaceChar); + writer.write(':'); + writer.write(davPropertyName.getName()); + writer.write("/>"); + } + writer.write(""); + } + writer.write(""); + writer.close(); + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + @Override + public String getName() { + return "PROPFIND"; + } + +} diff --git a/src/java/davmail/exchange/dav/ExchangePropPatchMethod.java b/src/java/davmail/exchange/dav/ExchangePropPatchMethod.java index cd24ef6b..6f749e8f 100644 --- a/src/java/davmail/exchange/dav/ExchangePropPatchMethod.java +++ b/src/java/davmail/exchange/dav/ExchangePropPatchMethod.java @@ -18,27 +18,22 @@ */ package davmail.exchange.dav; -import davmail.exchange.XMLStreamUtil; -import org.apache.commons.httpclient.*; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.RequestEntity; -import org.apache.jackrabbit.webdav.MultiStatusResponse; -import org.apache.jackrabbit.webdav.property.DefaultDavProperty; -import org.apache.jackrabbit.webdav.xml.Namespace; import org.apache.log4j.Logger; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamReader; -import java.io.*; -import java.util.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * Custom Exchange PROPPATCH method. * Supports extended property update with type. */ -public class ExchangePropPatchMethod extends PostMethod { +public class ExchangePropPatchMethod extends ExchangeDavMethod { protected static final Logger LOGGER = Logger.getLogger(ExchangePropPatchMethod.class); - static final String TYPE_NAMESPACE = "urn:schemas-microsoft-com:datatypes"; final Set propertyValues; @@ -51,33 +46,9 @@ public class ExchangePropPatchMethod extends PostMethod { public ExchangePropPatchMethod(String path, Set propertyValues) { super(path); this.propertyValues = propertyValues; - setRequestEntity(new RequestEntity() { - byte[] content; - - public boolean isRepeatable() { - return true; - } - - public void writeRequest(OutputStream outputStream) throws IOException { - if (content == null) { - content = generateRequestContent(); - } - outputStream.write(content); - } - - public long getContentLength() { - if (content == null) { - content = generateRequestContent(); - } - return content.length; - } - - public String getContentType() { - return "text/xml;charset=UTF-8"; - } - }); } + @Override protected byte[] generateRequestContent() { try { // build namespace map @@ -156,7 +127,6 @@ public class ExchangePropPatchMethod extends PostMethod { } catch (IOException e) { throw new RuntimeException(e); } - } @Override @@ -164,129 +134,4 @@ public class ExchangePropPatchMethod extends PostMethod { return "PROPPATCH"; } - List responses; - - @Override - protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) { - Header contentTypeHeader = getResponseHeader("Content-Type"); - if (contentTypeHeader != null && "text/xml".equals(contentTypeHeader.getValue())) { - responses = new ArrayList(); - XMLStreamReader reader; - try { - reader = XMLStreamUtil.createXMLStreamReader(new FilterInputStream(getResponseBodyAsStream()) { - final byte[] lastbytes = new byte[3]; - - @Override - public int read(byte[] bytes, int off, int len) throws IOException { - int count = in.read(bytes, off, len); - // patch invalid element name - for (int i = 0; i < count; i++) { - byte currentByte = bytes[off + i]; - if ((lastbytes[0] == '<') && (currentByte >= '0' && currentByte <= '9')) { - // move invalid first tag char to valid range - bytes[off + i] = (byte) (currentByte + 49); - } - lastbytes[0] = lastbytes[1]; - lastbytes[1] = lastbytes[2]; - lastbytes[2] = currentByte; - } - return count; - } - - }); - while (reader.hasNext()) { - reader.next(); - if (XMLStreamUtil.isStartTag(reader, "response")) { - handleResponse(reader); - } - } - - } catch (IOException e) { - LOGGER.error("Error while parsing soap response: " + e, e); - } catch (XMLStreamException e) { - LOGGER.error("Error while parsing soap response: " + e, e); - } - } - } - - protected void handleResponse(XMLStreamReader reader) throws XMLStreamException { - String href = null; - String responseStatus = ""; - while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "response")) { - reader.next(); - if (XMLStreamUtil.isStartTag(reader)) { - String tagLocalName = reader.getLocalName(); - if ("href".equals(tagLocalName)) { - href = reader.getElementText(); - } else if ("status".equals(tagLocalName)) { - responseStatus = reader.getElementText(); - } else if ("propstat".equals(tagLocalName)) { - MultiStatusResponse multiStatusResponse = new MultiStatusResponse(href, responseStatus); - handlePropstat(reader, multiStatusResponse); - responses.add(multiStatusResponse); - } - } - } - - } - - protected void handlePropstat(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException { - int propstatStatus = 0; - while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "propstat")) { - reader.next(); - if (XMLStreamUtil.isStartTag(reader)) { - String tagLocalName = reader.getLocalName(); - if ("status".equals(tagLocalName)) { - if ("HTTP/1.1 200 OK".equals(reader.getElementText())) { - propstatStatus = HttpStatus.SC_OK; - } else { - propstatStatus = 0; - } - } else if ("prop".equals(tagLocalName) && propstatStatus == HttpStatus.SC_OK) { - handleProperty(reader, multiStatusResponse); - } - } - } - - } - - - protected void handleProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException { - while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) { - reader.next(); - if (XMLStreamUtil.isStartTag(reader)) { - String tagLocalName = reader.getLocalName(); - Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI()); - multiStatusResponse.add(new DefaultDavProperty(tagLocalName, reader.getElementText(), namespace)); - } - } - } - - /** - * Get single Multistatus response. - * - * @return response - * @throws HttpException on error - */ - public MultiStatusResponse getResponse() throws HttpException { - if (responses == null || responses.size() != 1) { - throw new HttpException(getStatusLine().toString()); - } - return responses.get(0); - } - - /** - * Return method http status code. - * - * @return http status code - * @throws HttpException on error - */ - public int getResponseStatusCode() throws HttpException { - String responseDescription = getResponse().getResponseDescription(); - if ("HTTP/1.1 201 Created".equals(responseDescription)) { - return HttpStatus.SC_CREATED; - } else { - return HttpStatus.SC_OK; - } - } } diff --git a/src/java/davmail/exchange/dav/ExchangeSearchMethod.java b/src/java/davmail/exchange/dav/ExchangeSearchMethod.java new file mode 100644 index 00000000..ebab6cd6 --- /dev/null +++ b/src/java/davmail/exchange/dav/ExchangeSearchMethod.java @@ -0,0 +1,74 @@ +/* + * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway + * Copyright (C) 2012 Mickael Guessant + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package davmail.exchange.dav; + +import davmail.util.StringUtil; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameIterator; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.log4j.Logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * Custom Exchange PROPFIND method. + * Does not load full DOM in memory. + */ +public class ExchangeSearchMethod extends ExchangeDavMethod { + protected static final Logger LOGGER = Logger.getLogger(ExchangeSearchMethod.class); + + protected String searchRequest; + + public ExchangeSearchMethod(String uri, String searchRequest) throws IOException { + super(uri); + this.searchRequest = searchRequest; + } + + + + protected byte[] generateRequestContent() { + try { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(baos, "UTF-8"); + writer.write("\n"); + writer.write("\n"); + writer.write(" "); + writer.write(StringUtil.xmlEncode(searchRequest)); + writer.write("\n"); + writer.write(""); + writer.close(); + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + @Override + public String getName() { + return "SEARCH"; + } + +} diff --git a/src/java/davmail/http/DavGatewayHttpClientFacade.java b/src/java/davmail/http/DavGatewayHttpClientFacade.java index 7369bfbb..0475b34f 100644 --- a/src/java/davmail/http/DavGatewayHttpClientFacade.java +++ b/src/java/davmail/http/DavGatewayHttpClientFacade.java @@ -21,15 +21,15 @@ package davmail.http; import davmail.BundleMessage; import davmail.Settings; import davmail.exception.*; +import davmail.exchange.dav.ExchangeDavMethod; +import davmail.exchange.dav.ExchangeSearchMethod; import davmail.ui.tray.DavGatewayTray; -import davmail.util.StringUtil; import org.apache.commons.httpclient.*; import org.apache.commons.httpclient.auth.AuthPolicy; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.params.HttpClientParams; import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread; @@ -378,23 +378,7 @@ public final class DavGatewayHttpClientFacade { */ public static MultiStatusResponse[] executeSearchMethod(HttpClient httpClient, String path, String searchRequest, int maxCount) throws IOException { - String searchBody = "\n" + - "\n" + - " " + StringUtil.xmlEncode(searchRequest) + "\n" + - ""; - DavMethodBase searchMethod = new DavMethodBase(path) { - - @Override - public String getName() { - return "SEARCH"; - } - - @Override - protected boolean isSuccess(int statusCode) { - return statusCode == 207; - } - }; - searchMethod.setRequestEntity(new StringRequestEntity(searchBody, "text/xml", "UTF-8")); + ExchangeSearchMethod searchMethod = new ExchangeSearchMethod(path, searchRequest); if (maxCount > 0) { searchMethod.addRequestHeader("Range", "rows=0-" + (maxCount - 1)); } @@ -471,6 +455,39 @@ public final class DavGatewayHttpClientFacade { return responses; } + /** + * Execute webdav request. + * + * @param httpClient http client instance + * @param method webdav method + * @return Responses enumeration + * @throws IOException on error + */ + public static MultiStatusResponse[] executeMethod(HttpClient httpClient, ExchangeDavMethod method) throws IOException { + MultiStatusResponse[] responses = null; + try { + int status = httpClient.executeMethod(method); + + // need to follow redirects (once) on public folders + if (isRedirect(status)) { + method.releaseConnection(); + URI targetUri = new URI(method.getResponseHeader("Location").getValue(), true); + checkExpiredSession(targetUri.getQuery()); + method.setURI(targetUri); + status = httpClient.executeMethod(method); + } + + if (status != HttpStatus.SC_MULTI_STATUS) { + throw buildHttpException(method); + } + responses = method.getResponses(); + + } finally { + method.releaseConnection(); + } + return responses; + } + /** * Execute method with httpClient. *