diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java index 68d86244..35fc0341 100644 --- a/src/java/davmail/exchange/ExchangeSession.java +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -85,6 +85,7 @@ public class ExchangeSession { WELL_KNOWN_FOLDERS.add(DavPropertyName.create("sendmsg", URN_SCHEMAS_HTTPMAIL)); WELL_KNOWN_FOLDERS.add(DavPropertyName.create("drafts", URN_SCHEMAS_HTTPMAIL)); WELL_KNOWN_FOLDERS.add(DavPropertyName.create("calendar", URN_SCHEMAS_HTTPMAIL)); + WELL_KNOWN_FOLDERS.add(DavPropertyName.create("contacts", URN_SCHEMAS_HTTPMAIL)); } protected static final DavPropertyNameSet DISPLAY_NAME = new DavPropertyNameSet(); @@ -126,6 +127,7 @@ public class ExchangeSession { private String sendmsgUrl; private String draftsUrl; private String calendarUrl; + private String contactsUrl; /** * Base user mailboxes path (used to select folder) @@ -511,12 +513,14 @@ public class ExchangeSession { sendmsgUrl = getURIPropertyIfExists(properties, "sendmsg", URN_SCHEMAS_HTTPMAIL); draftsUrl = getURIPropertyIfExists(properties, "drafts", URN_SCHEMAS_HTTPMAIL); calendarUrl = getURIPropertyIfExists(properties, "calendar", URN_SCHEMAS_HTTPMAIL); + contactsUrl = getURIPropertyIfExists(properties, "contacts", URN_SCHEMAS_HTTPMAIL); LOGGER.debug("Inbox URL : " + inboxUrl + " Trash URL : " + deleteditemsUrl + " Sent URL : " + sentitemsUrl + " Send URL : " + sendmsgUrl + " Drafts URL : " + draftsUrl + - " Calendar URL : " + calendarUrl + " Calendar URL : " + calendarUrl + + " Contacts URL : " + contactsUrl ); } @@ -1983,7 +1987,7 @@ public class ExchangeSession { writer.write(participants.organizer); writer.write("\r\n"); } - // if not organizer, set REPLYTIME to force Outlook in attendee mode + // if not organizer, set REPLYTIME to force Outlook in attendee mode if (participants.organizer != null && !email.equalsIgnoreCase(participants.organizer)) { if (icsBody.indexOf("METHOD:") < 0) { icsBody = icsBody.replaceAll("BEGIN:VCALENDAR", "BEGIN:VCALENDAR\r\nMETHOD:REQUEST"); @@ -2394,6 +2398,112 @@ public class ExchangeSession { return results; } + /** + * Search users in contacts folder + * + * @param searchFilter search filter + * @return List of users + * @throws IOException on error + */ + public Map> contactFind(String searchFilter) throws IOException { + StringBuilder searchRequest = new StringBuilder(); + searchRequest.append("Select \"DAV:uid\", \"urn:schemas:contacts:email1\", \"urn:schemas:contacts:cn\"," + + " \"urn:schemas:contacts:givenName\",\"urn:schemas:contacts:sn\", \"urn:schemas:contacts:title\"," + + "\"urn:schemas:contacts:o\", \"urn:schemas:contacts:location\", \"urn:schemas:contacts:department\"," + + "\"urn:schemas:contacts:telephoneNumber\", \"urn:schemas:contacts:initials\", \"urn:schemas:contacts:street\"," + + "\"urn:schemas:contacts:st\", \"urn:schemas:contacts:c\", \"urn:schemas:contacts:mobile\"") + .append(" FROM Scope('SHALLOW TRAVERSAL OF \"").append(contactsUrl).append("\"')\n"); + if (searchFilter != null) { + searchRequest.append(" WHERE ").append(searchFilter); + } + MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeSearchMethod( + httpClient, URIUtil.encodePath(contactsUrl), searchRequest.toString()); + + Map> results = new HashMap>(); + Map item; + for (MultiStatusResponse response : responses) { + DavPropertySet properties = response.getProperties(HttpStatus.SC_OK); + // TODO: improve mapping + String uid = getPropertyIfExists(properties, "uid", Namespace.getNamespace("DAV:")); + String email1 = getPropertyIfExists(properties, "email1", Namespace.getNamespace("urn:schemas:contacts:")); + // patch email + if (email1 != null && email1.startsWith("\"")) { + int endIndex = email1.indexOf('\"', 1); + if (endIndex > 0) { + email1 = email1.substring(1, endIndex); + } + } + String cn = getPropertyIfExists(properties, "cn", Namespace.getNamespace("urn:schemas:contacts:")); + String givenName = getPropertyIfExists(properties, "givenName", Namespace.getNamespace("urn:schemas:contacts:")); + String sn = getPropertyIfExists(properties, "sn", Namespace.getNamespace("urn:schemas:contacts:")); + String title = getPropertyIfExists(properties, "title", Namespace.getNamespace("urn:schemas:contacts:")); + String company = getPropertyIfExists(properties, "o", Namespace.getNamespace("urn:schemas:contacts:")); + String location = getPropertyIfExists(properties, "location", Namespace.getNamespace("urn:schemas:contacts:")); + String department = getPropertyIfExists(properties, "department", Namespace.getNamespace("urn:schemas:contacts:")); + + String telephoneNumber = getPropertyIfExists(properties, "telephoneNumber", Namespace.getNamespace("urn:schemas:contacts:")); + String initials = getPropertyIfExists(properties, "initials", Namespace.getNamespace("urn:schemas:contacts:")); + String street = getPropertyIfExists(properties, "street", Namespace.getNamespace("urn:schemas:contacts:")); + String st = getPropertyIfExists(properties, "st", Namespace.getNamespace("urn:schemas:contacts:")); + String postalcode = getPropertyIfExists(properties, "postalcode", Namespace.getNamespace("urn:schemas:contacts:")); + String c = getPropertyIfExists(properties, "c", Namespace.getNamespace("urn:schemas:contacts:")); + String mobile = getPropertyIfExists(properties, "mobile", Namespace.getNamespace("urn:schemas:contacts:")); + item = new HashMap(); + if (uid != null) { + item.put("AN", uid); + } + if (email1 != null) { + item.put("EM", email1); + } + if (cn != null) { + item.put("DN", cn); + } + if (givenName != null) { + item.put("first", givenName); + } + if (sn != null) { + item.put("last", sn); + } + if (title != null) { + item.put("TL", title); + } + if (company != null) { + item.put("CP", company); + } + if (location != null) { + item.put("OFFICE", location); + } + if (department != null) { + item.put("department", department); + } + if (telephoneNumber != null) { + item.put("PH", telephoneNumber); + } + if (initials != null) { + item.put("initials", initials); + } + if (street != null) { + item.put("street", street); + } + if (st != null) { + item.put("state", st); + } + if (postalcode != null) { + item.put("zip", postalcode); + } + if (c != null) { + item.put("country", c); + } + if (mobile != null) { + item.put("mobile", mobile); + } + results.put(uid, item); + } + + LOGGER.debug("contactFind " + searchFilter + ": " + results.size() + " result(s)"); + return results; + } + /** * Get extended address book information for person with gallookup. * Does not work with Exchange 2007 @@ -2413,8 +2523,10 @@ public class ExchangeSession { // add detailed information if (!results.isEmpty()) { Map fullperson = results.get(person.get("AN").toLowerCase()); - for (Map.Entry entry : fullperson.entrySet()) { - person.put(entry.getKey(), entry.getValue()); + if (fullperson != null) { + for (Map.Entry entry : fullperson.entrySet()) { + person.put(entry.getKey(), entry.getValue()); + } } } } catch (IOException e) { diff --git a/src/java/davmail/ldap/LdapConnection.java b/src/java/davmail/ldap/LdapConnection.java index a4b0bac6..45a12391 100644 --- a/src/java/davmail/ldap/LdapConnection.java +++ b/src/java/davmail/ldap/LdapConnection.java @@ -1,5 +1,5 @@ /* - * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway +* DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway * Copyright (C) 2009 Mickael Guessant * * This program is free software; you can redistribute it and/or @@ -201,8 +201,25 @@ public class LdapConnection extends AbstractConnection { CRITERIA_MAP.put("apple-group-realname", "DP"); } + static final HashMap CONTACT_MAP = new HashMap(); + + static { + CONTACT_MAP.put("uid", "DAV:uid"); + CONTACT_MAP.put("mail", "urn:schemas:contacts:email1"); + CONTACT_MAP.put("displayname", "urn:schemas:contacts:cn"); + CONTACT_MAP.put("cn", "urn:schemas:contacts:cn"); + CONTACT_MAP.put("givenname", "urn:schemas:contacts:givenName"); + CONTACT_MAP.put("sn", "urn:schemas:contacts:sn"); + CONTACT_MAP.put("title", "urn:schemas:contacts:title"); + CONTACT_MAP.put("company", "urn:schemas:contacts:company"); + CONTACT_MAP.put("o", "urn:schemas:contacts:company"); + CONTACT_MAP.put("l", "urn:schemas:contacts:location"); + CONTACT_MAP.put("department", "urn:schemas:contacts:department"); + CONTACT_MAP.put("apple-group-realname", "urn:schemas:contacts:department"); + } + /** - * LDAP to Exchange Criteria Map + * LDAP filter attributes ignore map */ static final HashSet IGNORE_MAP = new HashSet(); @@ -504,6 +521,13 @@ public class LdapConnection extends AbstractConnection { if (session != null) { Map> persons = new HashMap>(); if (ldapFilter.isFullSearch()) { + // append personal contacts first + for (Map person : session.contactFind(null).values()) { + persons.put(person.get("AN"), person); + if (persons.size() == sizeLimit) { + break; + } + } // full search for (char c = 'A'; c < 'Z'; c++) { if (persons.size() < sizeLimit) { @@ -519,12 +543,20 @@ public class LdapConnection extends AbstractConnection { } } } else { - for (Map.Entry entry : ldapFilter.getOrFilterEntrySet()) { + // append personal contacts first + for (Map person : session.contactFind(ldapFilter.getContactSearchFilter()).values()) { + persons.put(person.get("AN"), person); + if (persons.size() == sizeLimit) { + break; + } + } + for (SimpleFilter simpleFilter : ldapFilter.getOrFilterSet()) { if (persons.size() < sizeLimit) { - for (Map person : session.galFind(entry.getKey(), entry.getValue().value).values()) { - if ((entry.getValue().operator == LDAP_FILTER_SUBSTRINGS) - || (entry.getValue().operator == LDAP_FILTER_EQUALITY && - entry.getValue().value.equalsIgnoreCase(person.get(entry.getKey())))) { + for (Map person : session.galFind(simpleFilter.getGalFindAttributeName(), simpleFilter.value).values()) { + if ((simpleFilter.operator == LDAP_FILTER_SUBSTRINGS) + // exclude non exact match on exact search + || (simpleFilter.operator == LDAP_FILTER_EQUALITY && + simpleFilter.value.equalsIgnoreCase(person.get(simpleFilter.getGalFindAttributeName())))) { persons.put(person.get("AN"), person); } if (persons.size() == sizeLimit) { @@ -584,7 +616,7 @@ public class LdapConnection extends AbstractConnection { LdapFilter ldapFilter = new LdapFilter(); if (reqBer.peekByte() == LDAP_FILTER_PRESENT) { String attributeName = reqBer.parseStringWithTag(LDAP_FILTER_PRESENT, isLdapV3(), null).toLowerCase(); - ldapFilter.addFilter(attributeName, new SimpleFilter()); + ldapFilter.addFilter(new SimpleFilter(attributeName)); if (!"objectclass".equals(attributeName)) { DavGatewayTray.warn(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER", ldapFilter.toString())); } @@ -654,7 +686,7 @@ public class LdapConnection extends AbstractConnection { } } - ldapFilter.addFilter(attributeName, new SimpleFilter(sValue, ldapFilterOperator)); + ldapFilter.addFilter(new SimpleFilter(attributeName, sValue, ldapFilterOperator)); } protected Set parseReturningAttributes(BerDecoder reqBer) throws IOException { @@ -885,28 +917,53 @@ public class LdapConnection extends AbstractConnection { final StringBuilder filterString = new StringBuilder(); int ldapFilterType; boolean isFullSearch = true; - final Map orCriteria = new HashMap(); - final Map andCriteria = new HashMap(); + final Set orCriteria = new HashSet(); + final Set andCriteria = new HashSet(); - public void addFilter(String attributeName, SimpleFilter simpleFilter) { - filterString.append('(').append(attributeName).append('=').append(simpleFilter.toString()).append(')'); + public void addFilter(SimpleFilter simpleFilter) { + filterString.append('(').append(simpleFilter.toString()).append(')'); + String attributeName = simpleFilter.getAttributeName(); - String exchangeAttributeName = CRITERIA_MAP.get(attributeName); - if (exchangeAttributeName != null) { + // known search attribute + if (CRITERIA_MAP.get(attributeName) != null) { isFullSearch = false; if (ldapFilterType == 0 || ldapFilterType == LDAP_FILTER_OR) { - orCriteria.put(exchangeAttributeName, simpleFilter); + orCriteria.add(simpleFilter); } else if (ldapFilterType == LDAP_FILTER_AND) { - andCriteria.put(exchangeAttributeName, simpleFilter); + andCriteria.add(simpleFilter); } + // full search (objectclass=*) filter } else if ("objectclass".equals(attributeName) && SimpleFilter.STAR.equals(simpleFilter.value)) { isFullSearch = true; + // ignore search attribute + } else if (IGNORE_MAP.contains(attributeName)) { + DavGatewayTray.debug(new BundleMessage("LOG_LDAP_IGNORE_FILTER_ATTRIBUTE", attributeName, simpleFilter.value)); + // unknown search attribute: warn } else { - if (IGNORE_MAP.contains(attributeName)) { - DavGatewayTray.debug(new BundleMessage("LOG_LDAP_IGNORE_FILTER_ATTRIBUTE", attributeName, simpleFilter.value)); - } else { - DavGatewayTray.warn(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER_ATTRIBUTE", attributeName, simpleFilter.value)); + DavGatewayTray.warn(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER_ATTRIBUTE", attributeName, simpleFilter.value)); + } + } + + public String getContactSearchFilter() { + if (orCriteria.isEmpty()) { + return null; + } else { + StringBuilder buffer = new StringBuilder(); + for (SimpleFilter simpleFilter : orCriteria) { + if (buffer.length() > 0) { + buffer.append(" OR "); + } + buffer.append('"').append(simpleFilter.getContactAttributeName()).append('"'); + if (simpleFilter.operator == LDAP_FILTER_EQUALITY) { + buffer.append(" = '").append(simpleFilter.value).append('\''); + } else if ("mail".equals(simpleFilter.getAttributeName())) { + buffer.append(" LIKE '\"").append(simpleFilter.value).append("%'"); + } else { + buffer.append(" LIKE '").append(simpleFilter.value).append("%'"); + } + } + return buffer.toString(); } } @@ -934,35 +991,54 @@ public class LdapConnection extends AbstractConnection { return isFullSearch; } - public Set> getOrFilterEntrySet() { - return orCriteria.entrySet(); + public Set getOrFilterSet() { + return orCriteria; } } static class SimpleFilter { static final String STAR = "*"; + final String attributeName; final String value; final int operator; - SimpleFilter() { + SimpleFilter(String attributeName) { + this.attributeName = attributeName; this.value = SimpleFilter.STAR; this.operator = LDAP_FILTER_SUBSTRINGS; } - SimpleFilter(String value, int ldapFilterOperator) { + SimpleFilter(String attributeName, String value, int ldapFilterOperator) { + this.attributeName = attributeName; this.value = value; this.operator = ldapFilterOperator; } @Override public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.append(attributeName); + buffer.append('='); if (SimpleFilter.STAR.equals(value)) { - return SimpleFilter.STAR; + buffer.append(SimpleFilter.STAR); } else if (operator == LDAP_FILTER_SUBSTRINGS) { - return SimpleFilter.STAR + value + SimpleFilter.STAR; + buffer.append(SimpleFilter.STAR).append(value).append(SimpleFilter.STAR); } else { - return value; + buffer.append(value); } + return buffer.toString(); + } + + public String getAttributeName() { + return attributeName; + } + + public String getGalFindAttributeName() { + return CRITERIA_MAP.get(attributeName); + } + + public String getContactAttributeName() { + return CONTACT_MAP.get(attributeName); } }