1
0
mirror of https://github.com/moparisthebest/davmail synced 2024-08-13 16:53:51 -04:00

Caldav: Implement basic Carddav search requests

git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@1035 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
mguessan 2010-05-06 19:26:44 +00:00
parent 4fbb040a35
commit 0faf2497db
3 changed files with 292 additions and 90 deletions

View File

@ -273,6 +273,10 @@ public class CaldavConnection extends AbstractConnection {
if (request.path.endsWith("/")) {
// GET request on a folder => build ics content of all folder events
String folderPath = request.getExchangeFolderPath();
ExchangeSession.Folder folder = session.getFolder(folderPath);
if (folder.isContact()) {
sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(session.getFolderResourceTag(folderPath)), "text/vcard", (byte[])null, true);
} else {
List<ExchangeSession.Event> events = session.getAllEvents(folderPath);
ChunkedResponse response = new ChunkedResponse(HttpStatus.SC_OK, "text/calendar;charset=UTF-8");
response.append("BEGIN:VCALENDAR\r\n");
@ -281,12 +285,12 @@ public class CaldavConnection extends AbstractConnection {
response.append("METHOD:PUBLISH\r\n");
for (ExchangeSession.Event event : events) {
String icsContent = StringUtil.getToken(event.getICS(), "BEGIN:VTIMEZONE", "END:VCALENDAR");
String icsContent = StringUtil.getToken(event.getBody(), "BEGIN:VTIMEZONE", "END:VCALENDAR");
if (icsContent != null) {
response.append("BEGIN:VTIMEZONE");
response.append(icsContent);
} else {
icsContent = StringUtil.getToken(event.getICS(), "BEGIN:VEVENT", "END:VCALENDAR");
icsContent = StringUtil.getToken(event.getBody(), "BEGIN:VEVENT", "END:VCALENDAR");
if (icsContent != null) {
response.append("BEGIN:VEVENT");
response.append(icsContent);
@ -295,14 +299,15 @@ public class CaldavConnection extends AbstractConnection {
}
response.append("END:VCALENDAR");
response.close();
}
} else {
ExchangeSession.Event event = session.getEvent(request.getExchangeFolderPath(), lastPath);
sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(event.getEtag()), "text/calendar;charset=UTF-8", event.getICS(), true);
ExchangeSession.Item item = session.getItem(request.getExchangeFolderPath(), lastPath);
sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(item.getEtag()), item.getContentType(), item.getBody(), true);
}
} else if (request.isHead()) {
// test event
ExchangeSession.Event event = session.getEvent(request.getExchangeFolderPath(), lastPath);
sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(event.getEtag()), "text/calendar;charset=UTF-8", (byte[]) null, true);
ExchangeSession.Item item = session.getItem(request.getExchangeFolderPath(), lastPath);
sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(item.getEtag()), item.getContentType(), (byte[]) null, true);
} else {
sendUnsupported(request);
}
@ -319,40 +324,54 @@ public class CaldavConnection extends AbstractConnection {
}
}
private void appendContactsResponses(CaldavResponse response, CaldavRequest request, List<ExchangeSession.Contact> contacts) throws IOException {
int size = contacts.size();
int count = 0;
for (ExchangeSession.Contact contact : contacts) {
DavGatewayTray.debug(new BundleMessage("LOG_LISTING_CONTACT", ++count, size));
DavGatewayTray.switchIcon();
appendItemResponse(response, request, contact);
}
}
protected void appendEventsResponses(CaldavResponse response, CaldavRequest request, List<ExchangeSession.Event> events) throws IOException {
int size = events.size();
int count = 0;
for (ExchangeSession.Event event : events) {
DavGatewayTray.debug(new BundleMessage("LOG_LISTING_EVENT", ++count, size));
DavGatewayTray.switchIcon();
appendEventResponse(response, request, event);
appendItemResponse(response, request, event);
}
}
protected void appendEventResponse(CaldavResponse response, CaldavRequest request, ExchangeSession.Event event) throws IOException {
protected void appendItemResponse(CaldavResponse response, CaldavRequest request, ExchangeSession.Item item) throws IOException {
StringBuilder eventPath = new StringBuilder();
eventPath.append(URIUtil.encodePath(request.getPath()));
if (!(eventPath.charAt(eventPath.length() - 1) == '/')) {
eventPath.append('/');
}
String eventName = StringUtil.xmlEncode(event.getName());
eventPath.append(URIUtil.encodeWithinQuery(eventName));
String itemName = StringUtil.xmlEncode(item.getName());
eventPath.append(URIUtil.encodeWithinQuery(itemName));
response.startResponse(eventPath.toString());
response.startPropstat();
if (request.hasProperty("calendar-data")) {
response.appendCalendarData(event.getICS());
if (request.hasProperty("calendar-data") && item instanceof ExchangeSession.Event) {
response.appendCalendarData(item.getBody());
}
if (request.hasProperty("getcontenttype")) {
response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
if (item instanceof ExchangeSession.Event) {
response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
} else if (item instanceof ExchangeSession.Contact) {
response.appendProperty("D:getcontenttype", "text/vcard");
}
}
if (request.hasProperty("getetag")) {
response.appendProperty("D:getetag", event.getEtag());
response.appendProperty("D:getetag", item.getEtag());
}
if (request.hasProperty("resourcetype")) {
response.appendProperty("D:resourcetype");
}
if (request.hasProperty("displayname")) {
response.appendProperty("D:displayname", eventName);
response.appendProperty("D:displayname", itemName);
}
response.endPropStatOK();
response.endResponse();
@ -365,8 +384,9 @@ public class CaldavConnection extends AbstractConnection {
* @param request Caldav request
* @param subFolder calendar folder path relative to request path
* @throws IOException on error
* @return Exchange folder object
*/
public void appendFolder(CaldavResponse response, CaldavRequest request, String subFolder) throws IOException {
public ExchangeSession.Folder appendFolder(CaldavResponse response, CaldavRequest request, String subFolder) throws IOException {
ExchangeSession.Folder folder = session.getFolder(request.getExchangeFolderPath(subFolder));
response.startResponse(URIUtil.encodePath(request.getPath(subFolder)));
@ -420,6 +440,7 @@ public class CaldavConnection extends AbstractConnection {
response.endPropStatOK();
response.endResponse();
return folder;
}
/**
@ -560,17 +581,21 @@ public class CaldavConnection extends AbstractConnection {
String folderPath = request.getExchangeFolderPath();
CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
response.startMultistatus();
appendFolder(response, request, null);
ExchangeSession.Folder folder = appendFolder(response, request, null);
if (request.getDepth() == 1) {
DavGatewayTray.debug(new BundleMessage("LOG_SEARCHING_CALENDAR_EVENTS", folderPath));
List<ExchangeSession.Event> events = session.getAllEvents(folderPath);
DavGatewayTray.debug(new BundleMessage("LOG_FOUND_CALENDAR_EVENTS", events.size()));
appendEventsResponses(response, request, events);
// Send sub folders for multi-calendar support under iCal, except for public folders
if (!folderPath.startsWith("/public")) {
List<ExchangeSession.Folder> folderList = session.getSubCalendarFolders(folderPath, false);
for (ExchangeSession.Folder folder : folderList) {
appendFolder(response, request, folder.folderPath.substring(folder.folderPath.indexOf('/') + 1));
if (folder.isContact()) {
appendContactsResponses(response, request, session.getAllContacts(folderPath));
} else {
DavGatewayTray.debug(new BundleMessage("LOG_SEARCHING_CALENDAR_EVENTS", folderPath));
List<ExchangeSession.Event> events = session.getAllEvents(folderPath);
DavGatewayTray.debug(new BundleMessage("LOG_FOUND_CALENDAR_EVENTS", events.size()));
appendEventsResponses(response, request, events);
// Send sub folders for multi-calendar support under iCal, except for public folders
if (!folderPath.startsWith("/public")) {
List<ExchangeSession.Folder> folderList = session.getSubCalendarFolders(folderPath, false);
for (ExchangeSession.Folder subFolder : folderList) {
appendFolder(response, request, subFolder.folderPath.substring(subFolder.folderPath.indexOf('/') + 1));
}
}
}
}
@ -630,9 +655,9 @@ public class CaldavConnection extends AbstractConnection {
// ignore cases for Sunbird
if (eventName != null && eventName.length() > 0
&& !"inbox".equals(eventName) && !"calendar".equals(eventName)) {
ExchangeSession.Event event;
event = session.getEvent(folderPath, eventName);
appendEventResponse(response, request, event);
ExchangeSession.Item item;
item = session.getItem(folderPath, eventName);
appendItemResponse(response, request, item);
}
} catch (HttpException e) {
DavGatewayTray.warn(new BundleMessage("LOG_EVENT_NOT_AVAILABLE", eventName, href));

View File

@ -106,12 +106,15 @@ public class ExchangeSession {
protected static final Namespace URN_SCHEMAS_HTTPMAIL = Namespace.getNamespace("urn:schemas:httpmail:");
protected static final Namespace SCHEMAS_EXCHANGE = Namespace.getNamespace("http://schemas.microsoft.com/exchange/");
protected static final Namespace SCHEMAS_MAPI_PROPTAG = Namespace.getNamespace("http://schemas.microsoft.com/mapi/proptag/");
protected static final Namespace URN_SCHEMAS_CONTACTS = Namespace.getNamespace("urn:schemas:contacts:");
protected static final DavPropertyNameSet EVENT_REQUEST_PROPERTIES = new DavPropertyNameSet();
static {
EVENT_REQUEST_PROPERTIES.add(DavPropertyName.create("permanenturl", SCHEMAS_EXCHANGE));
EVENT_REQUEST_PROPERTIES.add(DavPropertyName.GETETAG);
EVENT_REQUEST_PROPERTIES.add(DavPropertyName.create("contentclass", Namespace.getNamespace("DAV:")));
}
protected static final DavPropertyNameSet WELL_KNOWN_FOLDERS = new DavPropertyNameSet();
@ -605,6 +608,15 @@ public class ExchangeSession {
}
}
protected String getPropertyIfExists(DavPropertySet properties, DavPropertyName davPropertyName, String defaultValue) {
String value = getPropertyIfExists(properties, davPropertyName);
if (value == null) {
return defaultValue;
} else {
return value;
}
}
protected String getPropertyIfExists(DavPropertySet properties, DavPropertyName davPropertyName) {
DavProperty property = properties.get(davPropertyName);
if (property == null) {
@ -1876,20 +1888,181 @@ public class ExchangeSession {
}
/**
* Calendar event object
* Generic folder item.
*/
public class Event {
public abstract class Item {
protected String href;
protected String permanentUrl;
protected String displayName;
protected String etag;
protected String contentClass;
protected String noneMatch;
/**
* Create empty Item.
*/
public Item() {
}
/**
* Return item content type
* @return content type
*/
public abstract String getContentType();
/**
* Retrieve item body from Exchange
* @return item body
* @throws HttpException on error
*/
public abstract String getBody() throws HttpException;
/**
* Build Item instance from multistatusResponse info
* @param multiStatusResponse response
* @throws URIException on error
*/
public Item(MultiStatusResponse multiStatusResponse) throws URIException {
href = URIUtil.decode(multiStatusResponse.getHref());
permanentUrl = getPropertyIfExists(multiStatusResponse.getProperties(HttpStatus.SC_OK), "permanenturl", SCHEMAS_EXCHANGE);
etag = getPropertyIfExists(multiStatusResponse.getProperties(HttpStatus.SC_OK), "getetag", Namespace.getNamespace("DAV:"));
displayName = getPropertyIfExists(multiStatusResponse.getProperties(HttpStatus.SC_OK), "displayname", Namespace.getNamespace("DAV:"));
}
/**
* Get event name (file name part in URL).
*
* @return event name
*/
public String getName() {
int index = href.lastIndexOf('/');
if (index >= 0) {
return href.substring(index + 1);
} else {
return href;
}
}
/**
* Get event etag (last change tag).
*
* @return event etag
*/
public String getEtag() {
return etag;
}
protected HttpException buildHttpException(Exception e) {
String message = "Unable to get event " + getName() + " at " + permanentUrl + ": " + e.getMessage();
LOGGER.warn(message);
return new HttpException(message);
}
}
/**
* Calendar event object
*/
public class Contact extends Item {
/**
* Build Contact instance from multistatusResponse info
* @param multiStatusResponse response
* @throws URIException on error
*/
public Contact(MultiStatusResponse multiStatusResponse) throws URIException {
super(multiStatusResponse);
}
@Override
public String getContentType() {
return "text/vcard";
}
@Override
public String getBody() throws HttpException {
// first retrieve contact details
String result = null;
PropFindMethod propFindMethod = null;
try {
propFindMethod = new PropFindMethod(URIUtil.encodePath(permanentUrl));
DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propFindMethod);
MultiStatus responses = propFindMethod.getResponseBodyAsMultiStatus();
if (responses.getResponses().length > 0) {
DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
ICSBufferedWriter writer = new ICSBufferedWriter();
writer.writeLine("BEGIN:VCARD");
writer.writeLine("VERSION:3.0");
writer.write("UID:");
writer.writeLine(URIUtil.encodePath(getName()));
writer.write("FN:");
writer.writeLine(getPropertyIfExists(properties, DavPropertyName.create("cn", URN_SCHEMAS_CONTACTS), ""));
// RFC 2426: Family Name, Given Name, Additional Names, Honorific Prefixes, and Honorific Suffixes
writer.write("N:");
writer.write(getPropertyIfExists(properties, DavPropertyName.create("sn", URN_SCHEMAS_CONTACTS), ""));
writer.write(";");
writer.write(getPropertyIfExists(properties, DavPropertyName.create("givenName", URN_SCHEMAS_CONTACTS), ""));
writer.write(";");
writer.writeLine(getPropertyIfExists(properties, DavPropertyName.create("middlename", URN_SCHEMAS_CONTACTS), ""));
writer.write("TEL;TYPE=cell:");
writer.writeLine(getPropertyIfExists(properties, DavPropertyName.create("mobile", URN_SCHEMAS_CONTACTS), ""));
//writer.writeLine(getPropertyIfExists(properties, DavPropertyName.create("initials", URN_SCHEMAS_CONTACTS), ""));
// The structured type value corresponds, in sequence, to the post office box; the extended address;
// the street address; the locality (e.g., city); the region (e.g., state or province);
// the postal code; the country name
// ADR;TYPE=dom,home,postal,parcel:;;123 Main Street;Any Town;CA;91921-1234
writer.write("ADR;TYPE=home:;;");
writer.write(getPropertyIfExists(properties, DavPropertyName.create("homepostaladdress", URN_SCHEMAS_CONTACTS), ""));
writer.write(";;;");
writer.newLine();
writer.writeLine("END:VCARD");
result = writer.toString();
}
} catch (DavException e) {
throw buildHttpException(e);
} catch (IOException e) {
throw buildHttpException(e);
} finally {
if (propFindMethod != null) {
propFindMethod.releaseConnection();
}
}
return result;
}
}
/**
* Calendar event object
*/
public class Event extends Item {
/**
* ICS content
*/
protected String icsBody;
/**
* Build Event instance from multistatusResponse info
* @param multiStatusResponse response
* @throws URIException on error
*/
public Event(MultiStatusResponse multiStatusResponse) throws URIException {
super(multiStatusResponse);
}
/**
* Create empty event.
*/
public Event() {
}
@Override
public String getContentType() {
return "text/calendar;charset=UTF-8";
}
protected boolean isCalendarContentType(String contentType) {
return contentType.startsWith("text/calendar") || contentType.startsWith("application/ics");
}
@ -1977,7 +2150,8 @@ public class ExchangeSession {
* @return ICS (iCalendar) event
* @throws HttpException on error
*/
public String getICS() throws HttpException {
@Override
public String getBody() throws HttpException {
String result;
LOGGER.debug("Get event: " + permanentUrl);
// try to get PR_INTERNET_CONTENT
@ -2004,35 +2178,6 @@ public class ExchangeSession {
return result;
}
protected HttpException buildHttpException(Exception e) {
String message = "Unable to get event " + getName() + " at " + permanentUrl + ": " + e.getMessage();
LOGGER.warn(message);
return new HttpException(message);
}
/**
* Get event name (file name part in URL).
*
* @return event name
*/
public String getName() {
int index = href.lastIndexOf('/');
if (index >= 0) {
return href.substring(index + 1);
} else {
return href;
}
}
/**
* Get event etag (last change tag).
*
* @return event etag
*/
public String getEtag() {
return etag;
}
protected String fixTimezoneId(String line, String validTimezoneId) {
return StringUtil.replaceToken(line, "TZID=", ":", validTimezoneId);
}
@ -2630,8 +2775,8 @@ public class ExchangeSession {
LOGGER.warn("Unable to patch event to trigger activeSync push");
} else {
// need to retrieve new etag
Event newEvent = getEvent(href);
eventResult.etag = newEvent.etag;
Item newItem = getItem(href);
eventResult.etag = newItem.etag;
}
}
return eventResult;
@ -2639,6 +2784,38 @@ public class ExchangeSession {
}
}
/**
* Search contacts in provided folder.
*
* @param folderPath Exchange folder path
* @return list of contacts
* @throws IOException on error
*/
public List<Contact> getAllContacts(String folderPath) throws IOException {
String searchQuery = "Select \"DAV:getetag\", \"http://schemas.microsoft.com/exchange/permanenturl\", \"DAV:displayname\"" +
" FROM Scope('SHALLOW TRAVERSAL OF \"" + folderPath + "\"')\n" +
" WHERE \"DAV:contentclass\" = 'urn:content-classes:person'\n";
return getContacts(folderPath, searchQuery);
}
/**
* Search contacts in provided folder matching the search query.
*
* @param folderPath Exchange folder path
* @param searchQuery Exchange search query
* @return list of contacts
* @throws IOException on error
*/
protected List<Contact> getContacts(String folderPath, String searchQuery) throws IOException {
List<Contact> contacts = new ArrayList<Contact>();
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeSearchMethod(httpClient, URIUtil.encodePath(folderPath), searchQuery);
for (MultiStatusResponse response : responses) {
contacts.add(new Contact(response));
}
return contacts;
}
/**
* Search calendar messages in provided folder.
*
@ -2718,13 +2895,13 @@ public class ExchangeSession {
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeSearchMethod(httpClient, URIUtil.encodePath(folderPath), searchQuery);
for (MultiStatusResponse response : responses) {
String instancetype = getPropertyIfExists(response.getProperties(HttpStatus.SC_OK), "instancetype", Namespace.getNamespace("urn:schemas:calendar:"));
Event event = buildEvent(response);
Event event = new Event(response);
//noinspection VariableNotUsedInsideIf
if (instancetype == null) {
// check ics content
try {
event.getICS();
// getICS success => add event or task
event.getBody();
// getBody success => add event or task
events.add(event);
} catch (HttpException e) {
// invalid event: exclude from list
@ -2738,45 +2915,53 @@ public class ExchangeSession {
}
/**
* Get event named eventName in folder
* Get item named eventName in folder
*
* @param folderPath Exchange folder path
* @param eventName event name
* @param itemName event name
* @return event object
* @throws IOException on error
*/
public Event getEvent(String folderPath, String eventName) throws IOException {
String eventPath = folderPath + '/' + eventName;
Event event;
public Item getItem(String folderPath, String itemName) throws IOException {
String itemPath = folderPath + '/' + itemName;
Item item;
try {
event = getEvent(eventPath);
item = getItem(itemPath);
} catch (HttpNotFoundException hnfe) {
// failover for Exchange 2007 plus encoding issue
String decodedEventName = eventName.replaceAll("_xF8FF_", "/").replaceAll("_x003F_", "?").replaceAll("'", "''");
String decodedEventName = itemName.replaceAll("_xF8FF_", "/").replaceAll("_x003F_", "?").replaceAll("'", "''");
ExchangeSession.MessageList messages = searchMessages(folderPath, " AND \"DAV:displayname\"='" + decodedEventName + '\'');
if (!messages.isEmpty()) {
event = getEvent(messages.get(0).getPermanentUrl());
item = getItem(messages.get(0).getPermanentUrl());
} else {
throw hnfe;
}
}
return event;
return item;
}
/**
* Get event by url
* Get item by url
*
* @param eventPath Event path
* @param itemPath Event path
* @return event object
* @throws IOException on error
*/
public Event getEvent(String eventPath) throws IOException {
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(eventPath), 0, EVENT_REQUEST_PROPERTIES);
public Item getItem(String itemPath) throws IOException {
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(itemPath), 0, EVENT_REQUEST_PROPERTIES);
if (responses.length == 0) {
throw new DavMailException("EXCEPTION_EVENT_NOT_FOUND");
}
return buildEvent(responses[0]);
String contentClass = getPropertyIfExists( responses[0].getProperties(HttpStatus.SC_OK),
"contentclass", Namespace.getNamespace("DAV:"));
if ("urn:content-classes:person".equals(contentClass)) {
return new Contact(responses[0]);
} else if ("urn:content-classes:appointment".equals(contentClass)){
return new Event(responses[0]);
} else {
throw new DavMailException("EXCEPTION_EVENT_NOT_FOUND");
}
}
/**
@ -2804,15 +2989,6 @@ public class ExchangeSession {
return status;
}
protected Event buildEvent(MultiStatusResponse calendarResponse) throws URIException {
Event event = new Event();
event.href = URIUtil.decode(calendarResponse.getHref());
event.permanentUrl = getPropertyIfExists(calendarResponse.getProperties(HttpStatus.SC_OK), "permanenturl", SCHEMAS_EXCHANGE);
event.etag = getPropertyIfExists(calendarResponse.getProperties(HttpStatus.SC_OK), "getetag", Namespace.getNamespace("DAV:"));
event.displayName = getPropertyIfExists(calendarResponse.getProperties(HttpStatus.SC_OK), "displayname", Namespace.getNamespace("DAV:"));
return event;
}
private static int dumpIndex;
/**

View File

@ -95,6 +95,7 @@ LOG_LDAP_UNSUPPORTED_FILTER_ATTRIBUTE=Unsupported filter attribute: {0}= {1}
LOG_LDAP_UNSUPPORTED_FILTER_VALUE=Unsupported filter value
LOG_LDAP_UNSUPPORTED_OPERATION=Unsupported operation: {0}
LOG_LISTING_EVENT=Listing event {0}/{1}
LOG_LISTING_CONTACT=Listing event {0}/{1}
LOG_MESSAGE={0}
LOG_NEW_VERSION_AVAILABLE=A new version ({0}) of DavMail Gateway is available !
LOG_OPEN_LINK_NOT_SUPPORTED=Open link not supported (tried AWT Desktop and SWT Program)
@ -253,4 +254,4 @@ UI_USE_SYSTEM_PROXIES=Use system proxy settings :
UI_SHOW_STARTUP_BANNER=Display startup banner
UI_SHOW_STARTUP_BANNER_HELP=Whether to show the initial startup notification window or not
UI_IMAP_IDLE_DELAY=IDLE folder monitor delay (IMAP):
UI_IMAP_IDLE_DELAY_HELP=IMAP folder idle monitor delay in minutes, leave empty to disable IDLE support
UI_IMAP_IDLE_DELAY_HELP=IMAP folder idle monitor delay in minutes, leave empty to disable IDLE support