Find Contacts in Microsoft 365 using the Graph API and considerations for migrating from EWS - Part 1
In the 90’s and early 2000’s contacts management was relative easy if you just had Outlook and Microsoft Exchange OnPrem. Most people had contacts in one of these sources
Your personal contacts in your default contacts folder in a mailbox
Organization contacts where in the Global Address List for your org, you may have InterOrg Synchronization though one of the tools Microsoft provided which was always fun for Exchange Admin I personally had a love hate relationship with the Note connector for many years.
You may have Mail Enabled contacts in Active Directory for important external contacts
You may have a Public Folder Address list which could also be used as an Address list in Outlook
Possibly would could have had a Ldap address book for a different address source.
In 2024 I won’t list the number of sources contacts could be stored in or come from because they are way too numerous. Contact management in 2024 is full of pitfalls and is both difficult to understand and manage for both experienced and inexperienced developers.
How should I go about finding a contact using the Graph API
Because the locations and types of contacts are now more complex and the API’s are wanting in terms or useability and functionality , this is no longer a straight forward question to answer but lets look at some scenarios and solutions.
Straight forward search for a Contact in my personal contacts folder using the email address (or partial domain or alias).
Just do a filter using the Contacts endpoint in Graph (me/contacts or user@domain/contacts) eg using the Powershell Graph SDK
Get-mgusercontact -userid mailbox@domain.com -Filter "emailAddresses/any(a:a/address eq 'target@domain.com')"
If you want to do a partial search based on the domain instead of using the strongly typed emailAddresses property (which has some limitations when it comes to using more advanced lambda) you can look at using the PidLidEmail1OriginalDisplayName property as long as the address your searching for is the primary address for the contact. eg
The reason to target the PidLidEmail1OriginalDisplayName rather then Email1EmailAddress property is the later when you have a contact that was added from the Global Address list will be an EX Address while the OriginalDisplayName property will be as per the documentation
Description: Specifies the SMTP email address that corresponds to the email address for the Contact object.
To demonstrate this the top image is a contact that was added from the GAL where you can see the Email1EmailAddress is and EX address and below is just a normally UI entered contact.
So to do a wildcard search of all email addresses in a particular email domain you can use
get-mgusercontact -userid mailbox@domain.com -Filter "singleValueExtendedProperties/Any(ep: ep/id eq 'String {00062004-0000-0000-C000-000000000046} Id 0x8084' and contains(ep/value, '@domain.com'))"
Note this will return some false positives because its a contains rather then an endswith so you would get email with domain.com.au etc so some type of client side filtering is also needed. You can also do a partial based on StartsWith eg an alias
get-mgusercontact -userid mailbox@domain.com -Filter "singleValueExtendedProperties/Any(ep: ep/id eq 'String {00062004-0000-0000-C000-000000000046} Id 0x8084' and StartsWith(ep/value, 'gscales@'))"
Considerations for Migrating from EWS in this scenario
Where you would be using a SearchFilter in EWS (or a Restriction) which might look something like$SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ContactSchema]::EmailAddress1,$emEmailAdddrestoFind)
$frContactResults = $ContactsFolder.FindItems($SfSearchFilter,$Iview)
You would migrate to
Get-mgusercontact -userid mailbox@domain.com -Filter "emailAddresses/any(a:a/address eq 'target@domain.com')" which is a make the following Rest request
https://graph.microsoft.com/v1.0/user/mailbox@domain.com/contacts?$filter=emailAddresses/any(a:a/address eq 'target@domain.com'
Is using a Filter/Restriction still a good approach ~(not really but its the most direct method )~ the problem is Microsoft have made a number of changes recently https://techcommunity.microsoft.com/blog/microsoft_365blog/new-improved-contacts-in-outlook-on-the-web/3639773 . The first problem this is the only documentation they produce on what and how this works eg for something like Self-updating contacts
We also ensure that users can choose what information to persist in their contacts without losing any data. Users can choose whether they wish to keep their personal edits or override their edits with the suggested update. No more stale contacts!
So the Outlook UI can do that which is great but the very process talked about above is one that is more suited to an Application doing so (especially in the Generative AI era) at the moment they have failed to provide documentation or an API method to do that (and that post is from Sep 29, 2022). There is also no mention of the substrate process that synchronizes things and what if anything developers should be aware of when conflicts happen. In Part 2 I’ll look at that Search API in the Graph which covers things like migrating from ResolveName and Gal access in EWS and it’s more a better destination for searching for contacts but it also has a number of limitations.
My Applications Contacts are in a Public Folder
Using a Public folder to share contacts among a group of users was a simple and straight forward solution and when everyone mainly used Outlook classic it worked pretty well (You could even do a mod to use it in OWA in 2007). But as Public folders have been in a fairly long legacy cycle and new Outlook clients have become more the norm there a number reasons to stop using use Public Folders and move to a better solution. The Graph API doesn’t currently have the ability to access Public Folders so if you have Contacts in a Public folder and you can’t move them to something like a Shared Mailbox or Office365 Group or some other accessible location you are in a bit of a quandary if you need to Migrate away from using EWS (which given its upcoming depreciation is a bit of a must) . Its not an easy situation to be in and in most cases its not really a technical reason. One potentially solution is to do a gateway service where you essentially kick the problem from one app to another and in that gateway service you implement Public Folder access using Mapi, of if you have a client application you can’t change that is using EWS (eg developer went out of business etc) you could proxy the EWS request to the Graph requestsIf you are a third party developer that has used Public Folders apart from the user retraining aspect your not doing your users much benefit from sticking with this approach as moving to a solution where the contacts your creating would be accessible in the People hub/apps would open your contacts being accessible from many more clients (eg Outlook Mobile, OWA etc) including Teams (where as sticking with Public Folders is just heading your application to a situation where it will stop working at a point in the future).