Building On What’s Gone Before
“Imitation is the sincerest form of flattery that mediocrity can pay to greatness,” said Oscar Wilde. Leaving any notion of mediocrity aside, I liked the idea Sean McAvinue explained in his article about creating organizational contacts in user mailboxes using the Microsoft Graph API. It’s a simple concept: all organizations have contacts, like HR, the Travel Department, and Security, which users should be able to contact in case of emergency or other reasons. They can always look up the GAL, but creating personal contacts in user mailboxes means that mobile devices can synchronize the information too. All in all, a nice idea, and even better, Sean’s PowerShell code works.
Leveraging PowerShell
The beauty of PowerShell is that it’s easy to take an idea developed by someone else and evolve and improve it. That’s where imitation comes into play, and the idea that a script can be taken and enhanced is exactly what we want to encourage at Practical365.com. As I looked at the code, I thought of three improvements:
- Identify mailboxes created in the last month and create personal contacts in these mailboxes.
- Allow any mail-enabled recipient in Exchange Online to be used to create personal contacts.
- Check if personal contacts is present in a mailbox before attempting to create it.
These are relatively simple changes. Here’s what I did to create the code.
Find Target Mailboxes
To find mailboxes created in the last month, run the Get-ExoMailbox cmdlet with a server-side filter against the WhenMailboxCreated property. Date comparison can be tricky with the mailbox cmdlets, but the following code worked for me:
$LastMonth = (Get-Date).AddDays(-30) $Mailboxes = Get-ExoMailbox -Filter "WhenMailboxCreated -gt '$LastMonth'" -RecipientTypeDetails UserMailbox | Select ExternalDirectoryObjectId, DisplayName, UserPrincipalName
Find the Source for Personal Contacts
The idea is to allow any mail-enabled recipient to be a source for personal contacts. The Get-Recipient cmdlet can find any objects across all recipient types, but we need to identify which recipients should be used as a source. To do this, we mark the recipients to use by writing a value into a custom attribute. All mail-enabled objects support custom attributes, so we can use user mailboxes, shared mailboxes, Microsoft 365 Groups, distribution lists, mail users, and mail contacts. Assuming that the chosen recipients are updated with the value, we can find the set of source contacts with a command like:
$OrgContacts = Get-Recipient -Filter {CustomAttribute4 -eq "OrgContact"}
Updating Personal Contacts
The original script does the heavy lifting to create a JSON structure defining each personal contact to add to user mailboxes. My version does some parsing to handle each recipient type. The other change that’s implemented is to check if a personal contact exists in a mailbox before attempting to add it. The check uses a filter to check that a personal contact with the same email address exists. If one is not found, we go ahead and add it. Here’s the code for the check:
$Uri = "https://graph.microsoft.com/v1.0/users/$Mailbox/contacts?`$filter=emailAddresses/any(a:a/address eq '$Email')" $Results = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method "GET" -ContentType "application/json"
Figure 1 shows how the organizational contacts appear in a user mailbox. In this case, the origins for the contacts include a mail contact, distribution list, shared mailbox, Microsoft 365 group, and user mailbox.
![Organizational contacts created in a user mailbox](https://s40823.pcdn.co/wp-content/uploads/2021/06/Org-Contacts-1024x751.jpg)
The Next Step
Like any PowerShell script, I’m sure that this code (which you can download from GitHub) can be improved. For one thing, my scripts come with minimal error handling because I try to illustrate principles instead of writing off-the-shelf code. In any case, every organization has their own way of dealing with errors, logging issues, and so on. It’s best if you take the idea explained here and implement it in your own way.
Let your imagination run riot and see what you can do. All enhancements are welcome. No idea is a bad one. And when you’re done, be sure to share what you’ve built. We’ll all appreciate it.
is it possible to pull the existing (specific) contacts from user and add them to a folder automatically ?
Hi Tony,
I’m trying to create new contact records for employee emergency contacts. So need to add multiple phone numbers but it appears MSGraph v1.0 doesn’t support Assistant’s Phone and I’ve scoured the internet for example of how to update Business Phone, Business Phone 2, Business Phone 3, Home Phone, Home Phone 2 and so on without success.
Am I looking for something that doesn’t exist?
The Real Person!
The Real Person!
Here is the Graph documentation for the properties supported by a contact:
https://learn.microsoft.com/en-us/graph/api/resources/contact?view=graph-rest-1.0#properties. Business phones is listed as a string collection, so you should be able to pass a set of numnbers.
I’m on vacation for the next two weeks, otherwise I would look further.
Hi Tony, how could we save the contacts into a designated contact folder as opposed to the default using this script?
The Real Person!
The Real Person!
No idea. I haven’t done any research to know how to answer that question.
Hi Tony,
I am trying to include mobile numbers in this script and getting intermittent results. I have added mobilePhone to the contact object but it’s not including them. Any ideas?
$ContactObject = @”
{
“assistantName”: “$($Assistant)”,
“businessHomePage”: “$($HomePage)”,
“businessPhones”: [
“$($Phone)”],
“mobilePhone”: “$($MobilePhone)”,
“companyName”: “$($Company)”,
“department”: “$($Department)”,
“displayName”: “$($DisplayName)”,
“emailAddresses”: [
{
“address”: “$($Email)”,
“name”: “$($Displayname)”
}
],
“givenName”: “$($First)”,
“jobTitle”: “$($Title)”,
“middleName”: “$($Middle)”,
“nickName”: “$($Nickname)”,
“profession”: “$($Profession)”,
“personalNotes”: “$($OrgNotes)”,
“surname”: “$($Last)”,
“title”: “$($Saluation)”
}
“@
The Real Person!
The Real Person!
I think the issue that you might be running into is that the Mail Contact object returned by Exchange Online doesn’t include a mobile phone property. It has a Phone property, but not a mobile phone. Personal contacts of the type stored in your mailbox do support a mobile phone property. If you stored the mobile phone number in another property supported by the cmdlet, you’d be able to retrieve it.
Get-ExoRecipient -Identity Jack.Smith -PropertySet All | Fl *Phone*
Phone : +353 1 1947 144
Hi Tony, that has worked! Thank you!
It is ridiculous that Get-ExoRecipient can’t get this attribute. I think I will try to combine your script with Get-AzureADUser from the AzureAD module:
Connect-AzureAD -AccountId AADAdmin@mycompany.com
Get-AzureADUser -ObjectId SomeEmployee@mycompany.com | fl *mobile*
The Real Person!
The Real Person!
Using the Azure AD module is a bad idea because Microsoft deprecated the module in March 2024 and are due to retire it soon. Use the Get-MgUser cmdlet instead.
$MobileNumber = (Get-MgUser -UserId Joe.Soap@contoso.com).MobilePhone
Hi Tony, thank you for the great script:-) I have the problem that all contacts are created twice and the script recognizes only at the third execution that the contact already exists. Do you have any idea what is causing this?
The Real Person!
The Real Person!
No idea whatsoever. Have you tried to debug the script to see what happens as each line is executed?
Yes. Sorry! The error is in line 52. > $CheckTable = [System.Collections.Generic.List[Object]]::new() <. But only in the first and second run. Thanks for your reply!
The Real Person!
The Real Person!
That line just creates a PowerShell list object. It has nothing to do with creating contacts.
The Real Person!
The Real Person!
I updated https://github.com/12Knocksinna/Office365itpros/blob/master/PopulateOrgContacts.PS1 to make the check for existing mailbox contacts better. Maybe it will help.
Hi all, if anyone is having issues with not being able to set Contact’s Email Display name field, please see this post.
We’ve ran into an issue when creating personal contacts with Graph API with an email address, the Email address display name always gets set as Firstname Last name. This seems to be happening because its created as an EX instead of SMTP type. This only happens if a user exists with the same email address in the tenant’s Azure AD (either a regular or guest user account).
https://www.reddit.com/r/GraphAPI/comments/z5nbip/a_bug_or_a_feature_of_create_contact_action/
We are working with MS on this, if anyone else happens to find a solution for this, please reply to the above reddit post.
The Real Person!
The Real Person!
Sounds like Exchange Online is applying some form of template to the new object to create the first name last name display name. This would not be at all surprising…
We’ve been working with Microsoft and the feedback from the MS Graph Product team is that this happens by design and there are no ways to overcome this. The case is not closed as yet 🙂
The Real Person!
The Real Person!
Keep on pushing!
Hi Tony,
Thanks ! Awesome work !
I’m trying to took over your script to replace a process that was based on EWS and to cover the below needs:
– Org contacts based on a DL
– Create contacts on a specific contacts folder (not the default one)
– Create Specific contacts folder for new joiners
Things revamped: (to avoid Invoke-WebRequest)
– Using MSAL.PS to generate the Token based on a app (service principle)
– Using module Microsoft.Graph.Authentication to connect Graph
– Using module Microsoft.Graph.PersonalContacts to manage contacts and contacts folders
The issue that I’m facing is with permissions to be granted to make this all work.
Despite having granted the permissions “Contacts.ReadWrite”,”Contacts.Read” (Graph API) to the App registered and the Token seems to embed the right scopes (e.g. “Contacts.ReadWrite”,”Contacts.Read”), I’m getting Access denied while trying to manage contacts.
Here are the commands for which I’m getting the error message: “Access is denied. Check credentials and try again.”
– Get-MgUserContactFolder
– New-MgUserContactFolder
– Get-MgUserContactFolderContact
– New-MgUserContactFolderContact
I even tried the below command but it does not seems to work with service principles:
– Add-MailboxFolderPermission
Do you have an idea of what permission is missing to the app reg ?
In case, I can send the script.
Thanks in advance.
Cheers,
Patrick
The Real Person!
The Real Person!
Well, after signing in with the Contacts.ReadWrite permission, I was able to run this code interactively:
[array]$ContactsFolders = Get-MgUserContactFolder -UserId $UserId
$ContactFolder = $ContactsFolder | Where-Object {$_.DisplayName -eq “Contacts”}
[array]$Contacts = Get-MgUserContactFolderContact -ContactFolderId $ContactFolder.Id -UserId $UserId -All
Write-Host (“There are {0} contacts in the Contacts folder” -f $Contacts.count)
Everything worked – for my own mailbox. I expected this because I used the SDK interactively, so effectively I had delegate permission to my mailbox. An attempt to access any other mailbox resulted in an access is denied message. Have you assigned application permissions to your app? Contact.ReadWrite is available both as a delegated and application permission… https://learn.microsoft.com/en-us/graph/permissions-reference
Thanks for your reply.
This is exactly my issue, the access denied is especially with other mailboxes than mine.
I confirm that my app has both Contact.ReadWrite delegated and application permission .
I’m also delegating full access to all concerned mailboxes to my app that is also a service principal.
In case, I have attached the scrip here:
https://github.com/12Knocksinna/Office365itpros/files/10006062/script.txt
Thanks in advance.
Best,
Patrick
The Real Person!
The Real Person!
You can’t use application permissions when running the Microsoft Graph PowerShell SDK cmdlets interactively. You can only do so when running an app (or in Azure Automation with either a run as account or managed identity) with consent granted to the necessary application permissions.
The Real Person!
The Real Person!
The need for apps to use application permissions is covered in in https://practical365.com/connect-microsoft-graph-powershell-sdk/
Hi Tony,
Does that mean we cannot use such script from a runbook because the Graph SDK does not support interactive connection ?
I see from the MS documentation that app-only authentication is supported:
https://learn.microsoft.com/en-us/powershell/microsoftgraph/app-only?view=graph-powershell-1.0&tabs=powershell
Therefore, rather than using the token, I’m connecting as follows:
$Connection = Get-AutomationConnection -Name “AzureRunAsConnectionGraph”
Connect-MgGraph -ClientID $Connection.ApplicationId -TenantId $Connection.TenantId -CertificateThumbprint $Connection.CertificateThumbprint
$context = Get-MgContext
$context.Scopes
Here is the output:
Welcome To Microsoft Graph!
TeamMember.Read.All
User.ReadWrite.All
Group.Read.All
Directory.Read.All
GroupMember.Read.All
Team.ReadBasic.All
MailboxSettings.Read
Contacts.ReadWrite
Mail.Send
MailboxSettings.ReadWrite
Contacts.Read
Despite using the app-only authentication, I’m still getting the same access is denied errors.
Which makes me think there’s still something missing with permissions.
Do you have the same behavior using the app-only authentication ?
Again many thanks !
Cheers,
Patrick
The Real Person!
The Real Person!
No, you can use SDK cmdlets with Azure Automation. There are several articles covering that topic on this site, including https://practical365.com/microsoft-graph-sdk-powershell-azure-automation/.
The SDK does support interactive use, but as explained in https://practical365.com/connect-microsoft-graph-powershell-sdk/, the way that the service principal used by the SDK accrues permissions and the use of delegate permissions (a good thing, in my view) make it a development environment rather than one used for production.
I have followed your articles but I’m still getting the Access is denied error 🙁
The Real Person!
The Real Person!
Well, you know it’s a permission error. Can you use the cmdlets to interact with contacts in your own mailbox through an interaction session? If so, you’ll know that the code works with delegate permissions. The next step is to move to an app that has consented application permissions and try the code against a different mailbox.
Hi Tony,
Im working on implementing the scrit based on my circumstances with great success! Following the adding of contacts , I would love to know if there is an easy way to delete a contact with a given ID?
So i filled a dummy office user with 500 contacts and while doing so, I gave every contact a specific ID. I would like to implement a script, that gets a csv file with the info (ID) to 5 contacts of those 500 to delete them specifically.
Any ideas or solutions to my problem? Thank you in advance!
The Real Person!
The Real Person!
I’d use the Get and Remove options for the Contact resource type to find and delete the contact. https://learn.microsoft.com/en-us/graph/api/contact-get?view=graph-rest-1.0&tabs=http
Is there a way to update the Contact, if the Phone number changes? For example a User is already added as contact, however his Phone number changes.
could you also specifically delete a contact if it does not have the the Attribute anymore?
The Real Person!
The Real Person!
Sure. You’d use the Update Contact API to update the phone number: https://docs.microsoft.com/en-us/graph/api/contact-update?view=graph-rest-1.0&tabs=http
You can also delete contacts if you check them and find the information outdated or incorrect.
Hi Tony,
THANKS for this great script. This helped me alot!
I have tried to add the address to my contact but failed. Do you have an idea or tip how to do that?
THANKS
Markus
The Real Person!
The Real Person!
Do you have the necessary Graph permissions to run the command? Did you get an error message? Can you run the code step by step to find where the problem lies?
Hi ,
thx for sharing the code.
How can I try the code for a specific user ?
The Real Person!
The Real Person!
replace the line:
$Mailboxes = Get-ExoMailbox
with $Mailboxes = the user principal names of the mailboxes to process…. Like [array]$mailboxes = “Kate.Smith@xtz.com”, “Jack.Jones@me.com”
thank you, but when I try to execute the code again, something going wrong
“Invoke-RestMethod : Error on remote server: (400) Request not valid.
Proceeding…
Error creating contact for Error on remote server: (405) Method not allowed..
I’ve already done the graph authorization on API request, there’s something missing?
The Real Person!
The Real Person!
I don’t know how you have set things up, but I bet that the loop ForEach ($Mbx in $Mailboxes) is not working because the input array doesn’t contain all the information used to figure out mailbox details etc.
If $Mbx contains a UPN, then it will probably be sufficient to add a line:
$Mbx = Get-Mailbox -Identity $Mbx
Just under the ForEach line. That command will populate the $MBX variable with the information needed later on.
Have you stepped through the code to find out what is being passed in the Invoke-RestMethod call to see what’s failing? This is PowerShell, so it’s relatively straightforward to debug.
Awesome!
It Works fine!
It was really this, $Mbx = Get-Mailbox -Identity $Mbx
Thank you so much!
Hello Adriano, could you tell me which line you adjusted in the code?
Because the checking if the Contact is existing fails, it just goes on to create it, even though a contact with existing mail adress exists.
Hi,
I’m highly interested in applying the solution. Do you have a Youtube video that explains the process?
The Real Person!
The Real Person!
Nope. The process is fully explained in the article. I don’t see how YouTube would add any value to the discussion.