From c08d2439f0c26ce1775acaa75ee9b150276a430f Mon Sep 17 00:00:00 2001 From: Dominic White Date: Mon, 13 May 2024 20:53:21 +0200 Subject: [PATCH] Add support for retrieving user certificates It's possible to add a public S/MIME certificate for a user to the GAL. These are used when a user wants to encrypt a mail to another user, or validate their signature. The public certificate of the recipient is required. Being able to look them up rather than engage in a manual or offline synchornisation process makes this easier, as well as fetching updated certificates when they're changed. The bulk of this work was done by @krutelp in https://github.com/mguessan/davmail/pull/98 I merely extended it to support the UserSMIMECertificate field in addition to the MSExchangeCertificate field. These are both part of the EWS Contact: https://learn.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.contact?view=exchange-ews-api I tried to do it without using ContactDataShape.AllProperties but like @krutelp couldn't find a method that would return the certificates. I extended the ignored tags based on what was returned by our Microsoft365 instance, but not all of the fields listed under the Contact type above. I slightly modified the original PR to use Dos line endings in ResolveNamesMethod.java so exact changes could be observed instead of the entire file being changed. I also added the keys to the contact in ExchangeSession.java as KEY1 and KEY2. Finally, I undid the small changes in LdapConnection.java to the isMatch() methods to have them take an ExchangeSession.Contact and put them back to Map. This was mostly done to limit the changes in the patch to those necessary. --- .../davmail/exchange/ExchangeSession.java | 9 ++++ .../exchange/ews/EwsExchangeSession.java | 2 + src/java/davmail/exchange/ews/Field.java | 3 ++ .../exchange/ews/ResolveNamesMethod.java | 53 +++++++++++++++++++ src/java/davmail/ldap/LdapConnection.java | 17 ++++++ 5 files changed, 84 insertions(+) diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java index fbead6a2f..1c9c46e9e 100644 --- a/src/java/davmail/exchange/ExchangeSession.java +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -2020,6 +2020,9 @@ public String getBody() { writer.writeLine("PHOTO;TYPE=" + contactPhoto.contentType + ";ENCODING=BASE64:"); writer.writeLine(contactPhoto.content, true); } + + writer.appendProperty("KEY1;X509;ENCODING=BASE64", get("msexchangecertificate")); + writer.appendProperty("KEY2;X509;ENCODING=BASE64", get("usersmimecertificate")); writer.endCard(); return writer.toString(); @@ -2798,6 +2801,10 @@ protected ItemResult createOrUpdateContact(String folderPath, String itemName, S } else if ("PHOTO".equals(property.getKey())) { properties.put("photo", property.getValue()); properties.put("haspicture", "true"); + } else if ("KEY1".equals(property.getKey())) { + properties.put("msexchangecertificate", property.getValue()); + } else if ("KEY2".equals(property.getKey())) { + properties.put("usersmimecertificate", property.getValue()); } } LOGGER.debug("Create or update contact " + itemName + ": " + properties); @@ -3012,6 +3019,8 @@ public String getAlias() { CONTACT_ATTRIBUTES.add("private"); CONTACT_ATTRIBUTES.add("sensitivity"); CONTACT_ATTRIBUTES.add("fburl"); + CONTACT_ATTRIBUTES.add("msexchangecertificate"); + CONTACT_ATTRIBUTES.add("usersmimecertificate"); } protected static final Set DISTRIBUTION_LIST_ATTRIBUTES = new HashSet<>(); diff --git a/src/java/davmail/exchange/ews/EwsExchangeSession.java b/src/java/davmail/exchange/ews/EwsExchangeSession.java index 9925eff28..864918ea0 100644 --- a/src/java/davmail/exchange/ews/EwsExchangeSession.java +++ b/src/java/davmail/exchange/ews/EwsExchangeSession.java @@ -2975,6 +2975,8 @@ protected void internalExecuteMethod(EWSMethod ewsMethod) throws IOException { GALFIND_ATTRIBUTE_MAP.put("homePhone", "HomePhone"); GALFIND_ATTRIBUTE_MAP.put("pager", "Pager"); + GALFIND_ATTRIBUTE_MAP.put("msexchangecertificate", "MSExchangeCertificate"); + GALFIND_ATTRIBUTE_MAP.put("usersmimecertificate", "UserSMIMECertificate"); } protected static final HashSet IGNORE_ATTRIBUTE_SET = new HashSet<>(); diff --git a/src/java/davmail/exchange/ews/Field.java b/src/java/davmail/exchange/ews/Field.java index 1fab30a9b..3b7eeffd7 100644 --- a/src/java/davmail/exchange/ews/Field.java +++ b/src/java/davmail/exchange/ews/Field.java @@ -272,6 +272,9 @@ private Field() { // attachments FIELD_MAP.put("attachments", new UnindexedFieldURI("item:Attachments")); + // user certificate + FIELD_MAP.put("msexchangecertificate", new UnindexedFieldURI("contacts:MSExchangeCertificate")); + FIELD_MAP.put("usersmimecertificate", new UnindexedFieldURI("contacts:UserSMIMECertificate")); } /** diff --git a/src/java/davmail/exchange/ews/ResolveNamesMethod.java b/src/java/davmail/exchange/ews/ResolveNamesMethod.java index 58f653406..070b96fb7 100644 --- a/src/java/davmail/exchange/ews/ResolveNamesMethod.java +++ b/src/java/davmail/exchange/ews/ResolveNamesMethod.java @@ -38,6 +38,7 @@ public ResolveNamesMethod(String value) { super("Contact", "ResolveNames", "ResolutionSet"); addMethodOption(SearchScope.ActiveDirectory); addMethodOption(RETURN_FULL_CONTACT_DATA); + addMethodOption(ContactDataShape.AllProperties); unresolvedEntry = new ElementOption("m:UnresolvedEntry", value); } @@ -88,6 +89,27 @@ protected void handleContact(XMLStreamReader reader, Item responseItem) throws X handlePhysicalAddresses(reader, responseItem); } else if ("PhoneNumbers".equals(tagLocalName)) { handlePhoneNumbers(reader, responseItem); + } else if ("MSExchangeCertificate".equals(tagLocalName) + || "UserSMIMECertificate".equals(tagLocalName)) { + handleUserCertificate(reader, responseItem, tagLocalName); + } else if ("ManagerMailbox".equals(tagLocalName) + || "Attachments".equals(tagLocalName) + || "Photo".equals(tagLocalName) + || "Notes".equals(tagLocalName) + || "HasPicture".equals(tagLocalName) + || "DirectoryId".equals(tagLocalName) + || "Alias".equals(tagLocalName) + || "Categories".equals(tagLocalName) + || "InternetMessageHeaders".equals(tagLocalName) + || "ResponseObjects".equals(tagLocalName) + || "ExtendedProperty".equals(tagLocalName) + || "EffectiveRights".equals(tagLocalName) + || "CompleteName".equals(tagLocalName) + || "Children".equals(tagLocalName) + || "Companies".equals(tagLocalName) + || "ImAddresses".equals(tagLocalName) + || "DirectReports".equals(tagLocalName)) { + skipTag(reader, tagLocalName); } else { responseItem.put(tagLocalName, XMLStreamUtil.getElementText(reader)); } @@ -133,6 +155,37 @@ protected void handlePhoneNumbers(XMLStreamReader reader, Item responseItem) thr } } + protected void handleUserCertificate(XMLStreamReader reader, Item responseItem, String contextTagLocalName) throws XMLStreamException { + boolean firstValueRead = false; + String certificate = ""; + while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, contextTagLocalName)) { + reader.next(); + if (XMLStreamUtil.isStartTag(reader)) { + String tagLocalName = reader.getLocalName(); + if ("Base64Binary".equals(tagLocalName)) { + String value = reader.getElementText(); + if ((value != null) && !value.isEmpty()) { + if (!firstValueRead) { + // Only first certificate value will be read + certificate = value; + } else { + LOGGER.debug("ResolveNames multiple certificates found, tagLocaleName=" + + contextTagLocalName + " Certificate [" + value + "] ignored"); + } + } + } + } + } + responseItem.put(contextTagLocalName, certificate); + } + + protected void skipTag(XMLStreamReader reader, String tagLocalName) throws XMLStreamException { + LOGGER.debug("ResolveNames tag parsing skipped. tagLocalName=" + tagLocalName); + while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, tagLocalName)) { + reader.next(); + } + } + @Override protected void handleEmailAddresses(XMLStreamReader reader, Item responseItem) throws XMLStreamException { while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "EmailAddresses")) { diff --git a/src/java/davmail/ldap/LdapConnection.java b/src/java/davmail/ldap/LdapConnection.java index aab33e0bb..ad1f05d4d 100644 --- a/src/java/davmail/ldap/LdapConnection.java +++ b/src/java/davmail/ldap/LdapConnection.java @@ -26,6 +26,7 @@ import davmail.exchange.ExchangeSessionFactory; import davmail.exchange.dav.DavExchangeSession; import davmail.ui.tray.DavGatewayTray; +import davmail.util.IOUtil; import org.apache.log4j.Logger; import javax.naming.InvalidNameException; @@ -122,6 +123,8 @@ public class LdapConnection extends AbstractConnection { CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeStreet", "mozillahomestreet"); CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("businesshomepage", "mozillaworkurl"); CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("nickname", "mozillanickname"); + CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("msexchangecertificate", "msexchangecertificate;binary"); + CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("usersmimecertificate", "usersmimecertificate;binary"); } /** @@ -287,6 +290,10 @@ public class LdapConnection extends AbstractConnection { // iCal search attribute LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-serviceslocator", "apple-serviceslocator"); + + LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("msexchangecertificate;binary", "msexchangecertificate"); + LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("usersmimecertificate;binary", "usersmimecertificate"); + } /** @@ -967,6 +974,8 @@ protected void sendEntry(int currentMessageId, String dn, Map at for (Object value : (Iterable) values) { responseBer.encodeString((String) value, isLdapV3()); } + } else if (values instanceof byte[]) { + responseBer.encodeOctetString((byte[])values, BerEncoder.ASN_OCTET_STR); } else { throw new DavMailException("EXCEPTION_UNSUPPORTED_VALUE", values); } @@ -1688,6 +1697,14 @@ protected void sendPersons(int currentMessageId, String baseContext, Map