Syncing contacts across multiple platforms is a persistent business challenge, often addressed using a variety of custom automation solutions. These can range from custom PowerShell scripts and full-blown ISV applications to low-code/no-code Power Platform tools and client-side apps.
One thing these apps typically share is their reliance on EWS to create or retrieve contact information from Exchange. Because of the upcoming deprecation of EWS in October this year, these applications must be updated, rewritten, or reengineered to ensure that contact synchronization continues to function.
The architecture of these synchronization apps can vary significantly. For example:
Public Folders: Some apps use Public Folders as a central repository, often because they can still serve as an Address Book provider in Outlook Classic.
CSV Files: Others rely on CSV files, which have long been the universal format for importing and exporting data across most systems.
vCard Format: Some utilize the vCard format, a long-standing standard for contact data interchange.
Direct API Integration: Many pull data directly from one source API (e.g., SharePoint, Salesforce) and write it straight to another (e.g., Exchange).”
At its core, contact synchronization is a people issue. The way in which people need to consume these contacts (primary due to the client they use) is usually the reason these solutions have been put in place. For example, a Team or Organization might need a CRM customer list available in Outlook Classic, or field workers might require specific contacts synced directly to their mobile devices.
Because of this, a migration strategy must look beyond just the technology side and at how the end users are consuming the contacts. Moving a Public Folder sync to a Shared Mailbox might technically solve the problem on paper, but if the solution isn’t useable or the change is too hard for users to adapt to then this makes the new solution an effective failure.
To this end I’ve split the article across a few parts where we’ll look at some real implementations and talk about some of challenges and things to look out for.
Whatever method you use to automate synchronization, they all share a foundational step: creating the actual contacts inside a mailbox. This is precisely where EWS is probably handling the heavy lifting and now needs to be retrofitted. While creating a single contact is relatively straightforward, handling them at scale is a different story. High-volume syncing requires you to implement batching and remain highly aware of connection limits in Microsoft and concurrent throttling restrictions.
If you know what you’re doing and have a clear picture of the expected output, prompting an AI to help craft a custom PowerShell script using the Microsoft Graph PowerShell SDK is probably now the best and most accepted approach. That said, if you aren’t 100% familiar with the specific nuances of Graph contact creation, you can run into a few unexpected snags—though it is generally a well-understood domain for most LLMs and should work okay.
Over the years, I’ve written a lot of contact sync scripts. Back in 2010, I came up with this flexible Exchange contact creation method. I personally liked this approach because it simplified the process while still allowing you to maintain full fidelity across all contact properties. For this series, I teamed up with Claude to produce a modernized version of that script—one that uses the PowerShell Graph SDK and supports batching and connection limits. I have published the final result to both the PowerShell Gallery and GitHub so if you need a template of even something for you own LLM prompt to learn from this can be helpful.
I’ve published this script to the PowerShell Gallery
https://www.powershellgallery.com/packages/GCSContact/
https://github.com/gscales/Powershell-Scripts/tree/master/Graph101/GraphSDK/GCSContact
Now we have the ability to create contacts let look at use cases
One side note is that there is another method you could use to create Contact which is the Mailbox Import/Export endpoint. This can be useful when you copying contacts mailbox to mailbox.
This is a pretty common ask I’ve seen over the years, one the of the more popular PowerShell modules I wrote for EWS was https://github.com/gscales/Powershell-Scripts/tree/master/EWSContacts which had some functions in it to do just that. The EWS Managed API helped make this a little easier because of the way they local types worked but this was more a useful quirk than a intended feature.
Since Exchange 2000 the directory for Exchange has been Active Directory and the GAL is a LDAP filter of those directory object. While Microsoft 365 obfuscates the backend details this is still how it works in the substrate.
As I mentioned earlier, contact synchronization is ultimately a people issue, and there are a few different ways to pull address data from Exchange. A classic reason you might need the GAL copied directly to a local contacts folder is offline availability—ensuring you have everyone’s mobile number on your phone even when you don’t have data access, which can be critical in an emergency.
To keep this as real-world as possible, let’s look at this GitHub repository as an example, which I previously used in part one of my EWS module.
In this project, the source GAL information is gathered using the Exchange Online V3 cmdlets Get-User and Get-Contact. Looking at the properties they target, they are primarily concerned with just email addresses and mobile numbers. This makes total sense—you don’t want to bloat your local contacts with unnecessary data if you don’t have to.
If you wanted to migrate this to the Graph you would do something like the following (probably using LLM Prompt to build your own custom solution).
Step 1 Do your connections
Connect-ExchangeOnline (probably using certificate auth for seamless automation)
Connet-MgGraph (again use Application authentication for automation)
Step 2 Grab the Contact Data from the Directory
I’m distilling this down a bit based on this for my example I’m going to use
$ContactData = Get-User -ResultSize Unlimited -Filter “Phone -ne `$null -or MobilePhone -ne `$null” | Select-Object DisplayNameFirstName,LastName,Title,Company,Department,WindowsEmailAddress,Phone,MobilePhone
This will grab all the user objects and user mailboxes from the directory that have either a business or mobile phone number configured, selecting just the minimal set of attributes needed to spin up our local contacts.
Step 3 Sync the Data
For this process I recommend you have a custom Extended property on any contact that your creating in a Mailbox. This will always help you to identify the contacts you create and modify/delete/etc in the future.
In my module i have the Get-GCSContactIndex cmdlet which will enumerate all the contacts in the local contacts folder (or multiple folders if you have them) and then produced a hashtable so you can decide if you need to create the contact locally.
$ExistingContacts = Get-GCSContactIndex -UserId gscales@datarumble.com -Folderid Contacts
Now we need to find the contacts that don’t exist and batch those to create them
This add an extended property to any contact it creates and also set things like the subject property. This isn’t meant to be a full blown solution but a starter that can be improved and customized.
Conclusion Part 1
In this post, we’ve covered what is relatively low-hanging fruit, but also one of the most common real-world use cases: copying the GAL (or a subset of it) to a local mailbox (traditionally handled by our favourite "janky scripted solutions").If you are currently relying on EWS to do this, the clock is ticking—it’s time to migrate before your existing solutions stop working.
As you plan your migration to Microsoft Graph, keep these four key takeaways in mind:
Your source data feed doesn’t need to change: It is highly unlikely you were pulling directory data via EWS. If you were using EWS for something like
ResolveName, it’s a straightforward swap in Graph.Prioritize batching, but watch the limits: Use batching as much as possible to handle high volumes efficiently, but keep an eye on the current concurrent connection limit (historically capped at 4).
Lean on AI to accelerate development: Contact creation is a well-mapped domain. Because it’s a highly understood topic for LLMs, prompting an AI can save you a massive amount of time and effort when drafting your scripts.
Lock down your credentials: Take this opportunity to enforce the principle of least privilege. Unlike EWS—which often required sweeping mailbox access—Microsoft Graph allows you to strictly scope and restrict permissions to only what your application actually needs.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.