With many organizations now allowing users to work from anywhere and on any device, a new set of challenges has risen to the surface. For example, new users who work from outside the office benefit from knowing how to contact the IT Service Desk, HR, and other internal services.
This information is often easily available in the Global Address List (GAL). It becomes more challenging to find this information on a mobile device because email clients don’t usually have access to the GAL. For instance, Outlook mobile synchronizes only personal contacts to the device.
To solve the problem, I created a PowerShell script to read a set of standard contacts from a CSV file and write them as personal contacts to user mailboxes. Mobile devices can then synchronize these contacts along with others created by the user.
Creating a Registered App
The script uses Graph API to provision the contacts, so we need an app registered in Azure AD to hold permissions and authentication (here’s another example of using the Graph with PowerShell). Create a new App Registration in the Azure AD Portal and add the Contacts.ReadWrite application permission. Make sure to grant admin consent for the permission to allow the script to access the contacts for all users:
Moving back to the app properties, note the Application (client) ID and Directory (tenant) ID from the Overview page:
Finally, open the Certificates & Secrets page and create a new client secret. Note the value of the secret. The combination of app identifier, tenant identifier, and app secret will allow our script to authenticate and use the assigned permission to interact with user mailboxes.
Note: as the secret is used to authenticate the app registration, treat this secret like a password and be careful when storing it in plain text.
Prepare the Contact Import File
The script reads the list of contacts from a CSV file. A sample file is available on GitHub and contains the following columns:
- assistantName
- businessHomePage
- businessPhones
- companyName
- department
- emailAddress
- givenName
- jobTitle
- middleName
- nickName
- profession
- personalNotes
- surname
- title
Each of these values maps directly to the value you see on Outlook’s contact page. All values are optional, so if they aren’t needed, just leave them blank. A sample entry is already populated in the file and can be replaced with your first entry. You can add as many entries as you like into the CSV. The script will add each item in the CSV as a personal contact in a target mailbox:
Running the script
Once the import file is prepared, download the script from GitHub and save it locally. The script requires the following parameters:
- Mailbox – This is the UPN of the target mailbox.
- CSVPath – This is the full path to the import CSV.
- ClientID – This is the Application ID for the registered app.
- ClientSecret – this is the Secret value created earlier.
- TenantID – This is the Directory ID of the tenant.
With each parameter in hand, the script can be run for a single user by specifying the UPN of the target mailbox:
##Declare Parameters##
$mailbox = adminseanmc@adminseanmc.com
$clientSecret = <Client Secret Value>
$clientID = <Application (Client) ID Value>
$tenantID = <Directory (Tenant) ID Value>
$CSVPath = <Full path to the import CSV>
##Run Script##
.\graph-PopulateContactsFromCSV.ps1 -Mailbox $mailbox -ClientSecret $clientSecret -ClientID $clientID -TenantID $tenantID -CSVPath $csvPath
The script shows the details of each contact in JSON format as it is provisioned:
Some additional logic allows the script to add the set of contacts to all mailboxes. To do this, connect to the Exchange Online Management Shell and run as below. This will get a list of all user mailboxes in the environment and then run the import script for each of them:
##Declare Parameters##
$clientSecret = <Client Secret Value>
$clientID = <Application (Client) ID Value>
$tenantID = <Directory (Tenant) ID Value>
$CSVPath = <Full path to the import CSV>
##Run Script##
$mailboxes = Get-EXOMailbox -resultsize unlimited -RecipientTypeDetails usermailbox
Foreach($mailbox in $mailboxes){
.\graph-PopulateContactsFromCSV.ps1 -Mailbox $mailbox.UserPrincipalName -ClientSecret $clientSecret -ClientID $clientID -TenantID $tenantID -CSVPath $csvPath
}
Once the script finishes, the contacts will be in the users’ personal contacts and available to sync to their device:
Summary
With a small amount of preparatory work, this script makes it very easy to populate contacts for any number of users. The new contacts can be synchronized to mobile devices and available from anywhere.
Both the script and the import file sample are available on GitHub.
Note: As always, make sure to review and test any code before running it in a production environment.
Hi I found your script, which is very helpful and I have 1 question on the .csv file. Should it be encoded in UTF-8? Our company is from Europe, therefore we have some special characters in our names, but if I export the .csv in UTF-8, the script from you then imports the contacts with the diacritics replaced with question marks. Do you have any idea how to resolve this?
Thanks, great script, work like a charm !
If I want to create contact in a new contact folder (under root folder) with Create ContactFolder cmd, how can I integrate it ?
I find this ressource (https://learn.microsoft.com/en-us/graph/api/user-post-contactfolders?view=graph-rest-1.0&tabs=powershell) but fail to implement.
Hey
really cool script and I got it to run, but I would really love to add a 2nd phone number.
I already tried to modify the script and csv by adding
“mobilePhone”: [
“$($contact.mobilePhone)”,
to the
$ContactObject = @”
but as soon I do a change and run the script I get error code (400) Bad Request
Hi Yuki,
It may just be a syntax error in the JSON if it’s picking up a bad request. I haven’t tested but as mobile phone is a string value and not a collection like businessPhones, what you would need is:
“mobilePhone”: “$($contact.mobilePhone”,
https://learn.microsoft.com/en-us/graph/api/resources/contact?view=graph-rest-1.0
Great Script! How to you format JSON for multiple email addresses and email address display names?
Great script, Thanks! How do you format JSON is contact has multiple email addresses?
I have a bit of a quirk. I ran the script and used the sample csv provided.
If i click on Address Book in Outlook and use the Contacts drop down it only shows Bruce Wayne
But if i click on the contacts button or People button in Outlook it shows my pre-existing contacts.
Does this script in any way over write any existing contacts?
How to import in a different contact folder than the default one?
Hi Sean,
I want to use “+” as well in the phone number for the country code. If I have + then the script wont work. Is there a way to include + character in the csv file for phone numbers. That would be a great help Sean.
If you can suggest me anything that will help me a lot.
Hi Sean,
I want to use “+” as well in the phone number for the country code. If I have + then the script wont work. Is there a way to include + character in the csv file for phone numbers. That would be a great help Sean.
this was fantastic! thank you! i’ve been searching for a script to remove all contacts from there if we need to start over. got anything for that?
I don’t have one already created but if you use this as a base and look at using DELETE instead of POST that would work
Can I import bulk outlook contacts using JSON format
Yes you can, JSON is the format the code builds but with some modifications you could use JSON as in input file.
I am still running Exchange on-prem, is there a way to make this script run with Exchange On-Prem?
No this won’t work on prem as it uses the MS Graph. There are other solutions that will work on-prem such as EWS which you could check out.
Hi Tony,
I’, trying to import an address into Office365 via an XML file, but I always get this error. Does the import via XML still work at all in Office365? I have given all permissions to the service email in the tenant, but it looks like there are still some permissions missing or could it be the version of TLS?
Configuration XML-File:
Error:
Starting Folder Check with User ‘service-mail@XXX.com’
Exchange Connect OK
Folder Check Error:
Exception: Microsoft.Exchange.WebServices.Data.ServiceRequestException: The request failed. The underlying connection was closed: An unexpected error occurred on a send. —>
I don’t know. I never tried importing with an XML file.
Hi Sean,
I found out that the I am using country code in phone number. If I use the phone numbers only then the script works fine for all mailboxes. But I want to keep the country code as well with “+” I want the numbers in the same format as csv file.
Hi Sean,
Instead of doing it for a single user, can this be done to all the users contacts list without creating the duplicate ones.
When using this script, contacts are not updated, rather it creates duplicates. That is bad for us.
That is expected,
The script could be modified to check for existing contacts if you require to update existing ones.
Hi Sean,
How to check for the existing contacts and not create a duplicate for it?
Hi Sean,
first of all thank you for your script!
Unfortunately i am getting the following error:
Error creating contact bruce.wayne@WayneEnterprises.com for xxx@xxx.com Response status code does not indicate success: 403 (Forbidden).
I have tested and used the credentials for the App Registration for another script and that should not be the problem. Do you have any idea?
Thank you in Advance
G
Hi G,
Looks like you are most likely missing the Contact.readwrite permission – make sure it is an Application permission, not delegated.
Thank you so much Sean, what a stupid mistake on my side! Ofc it worked as a charm!
Well.. if you use the fat client Outlook you can use the addin Company Contacts for Outlook to have 1) a way better searchable global address list interface and 2) import the GAL into your Outlook contacts. Maybe helpful if you really want this functionality.
Hey,
Thank you for this job, my question :
is there a way to automate this directly from AD ?
You could always modify the script to import from AD as a source
Would you have any more details on this one? a brief “how to” perhaps? I would be very, very thankful!! Cheers!
At a high level I’d look at automating pulling the data from AD into a CSV and then using that CSV and the input for this code. It’s hard to say specifics given each environment will be different but focus on gathering the data first, then look at how this code can process it.
Hi Sean,
when I try to use the script, I’ve a 400 error at
Error creating contact for *******@***.com Errore del server remoto: (400) Richiesta non valida.
In C:\_SCRIPT\GRAPH-PopulateOrgContacts_fromCSV.ps1:168 car:9
+ throw “Error creating contact $($contact.emailaddress) for $m …
How can I solve this ?
thx
i have exactly same message, some one can help us please ?
sorry my error message is in french languge !
Error creating contact for **************r Le serveur distant a retourné une erreur : (400) Demande incorrecte.
Au caractère C:\Ahmed MEZRAG\graph-PopulateContactsFromCSV.ps1:182 : 9
+ throw “Error creating contact $($contact.emailaddress) for $m …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Error creating …nde incorrecte.:String) [], RuntimeException
+ FullyQualifiedErrorId : Error creating contact for ahmed.mezrag@eaudeparis.fr Le serveur distant a retourné une erreur : (400) Demande incorrecte.
This is most likely an error with your import file, things like special characters aren’t accounted for in the code. Have you tested with the sample contact in the CSV?
Hi Sean,
I’m also having the same issue…
I have created a new .CSV with the required fields and just writted test in one of them. The result is always the same.
Error:
https://graph.microsoft.com/v1.0/users/Dummy.User@*****/contacts
Error creating contact for Dummy.User@***** The remote server returned an error: (400) Bad Request.
At C:\temp\graph-PopulateContactsFromCSV.ps1:168 char:9
+ throw “Error creating contact $($contact.emailaddress) for $m …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo: OperationStopped: (Error creating …0) Bad Request.:String) [], RuntimeException
+ FullyQualifiedErrorId : Error creating contact for Dummy.User@***** The remote server returned an error: (400) Bad Request.
A colleague found out the error for the bad request error (400).
It seems in the contacts.csv file, you can’t separate by columns the information.
All info must be in the same column separated by a comma.
Example:
Name,Surname,email,…
John,River,john.river@contoso.com,…
A 400 error would imply that the permissions on the App Reg aren’t correct – make sure you have added them as Application Permissions
Hi Adriano,
I am having the same issue did you fixed it?
what does Error obtaining Token mean? I am sure that application’s registration steps has been done properly.
thanks
graph-PopulateContactsFromCSV.ps1
:205 char:5
That error would most likely mean that the App ID, Tenant ID or Client Secret are incorrect. You can try run the GetGraphToken function on it’s own to test.
Hi, great script. Thx!
How do you deal with changed users (e.g. marriage or address change)?
Hi Florian,
The code doesn’t account for changes but it can be adapted to do that no problem. I’d suggest checking out Tony Redmonds article where he adds to this:
https://practical365.com/create-org-contacts-powershell-graph/
Hello,
would it be possible to use contacts from a o365 account contacts folder instead of a csv?
Hi Alex,
It is indeed, the code would need to be adjusted for that. This article looks at provisioning from directory objects:
https://practical365.com/create-org-contacts-powershell-graph/
Does this script need to be run every time a new user is added?
Yes
hey Tony, do you know how to import contacts avoid contact’s duplication?
thanks
Michele
Hey Tony,
How to check for the existing contact so that it doesn’t create duplicate? This would be great help. What changes do I Have to make in script?
I guess you’d grab the set of contacts from the mailbox and check the new contact against the list before adding it. The Graph ListContacts API is what you’d use https://docs.microsoft.com/en-us/graph/api/user-list-contacts?view=graph-rest-1.0&tabs=http
Hi Tony,
I am able to create contacts from the above process. I have created a graph and the script works fine. I am beginner to this so, can you help me with the script. What change do I need to make in the above script so that duplicates aren’t created?
Thanks!!
Have a look at the script featured in https://practical365.com/create-org-contacts-powershell-graph/. It includes code to fetch existing contacts from a mailbox and load them into a table that is checked before attempting to add a new contact. This makes sure that you don’t add duplicates.
$Uri = “https://graph.microsoft.com/v1.0/users/” + $Mbx.ExternalDirectoryObjectId + “/contacts”
[array]$ContactsInMbx = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method “GET”
If ($ContactsInMbx.Value.Count -gt 0) { $ContactsInMbx = $ContactsInMbx.Value | Select-Object -ExpandProperty emailaddresses }
$CheckTable = [System.Collections.Generic.List[Object]]::new()
ForEach ($C in $ContactsInMbx) { $CheckTable.Add($C.Address.toString()) }
Hi Tony,
Thanks for the prompt reply. I used the Sean script to add contacts and again run the other script by him to delete any duplicates in the mailbox. It worked perfectly fine for me and this is what I was looking for. But I tried to run the script for all users, it only runs for the first user in the exchange mailbox list and foreach statement doesn’t seem to work. Can you tell me what’s going wrong here.
$mailboxes = Get-EXOMailbox -resultsize unlimited -RecipientTypeDetails usermailbox
Foreach($mailbox in $mailboxes){
.\graph-PopulateContactsFromCSV.ps1 -Mailbox $mailbox.UserPrincipalName -ClientSecret $clientSecret -ClientID $clientID -TenantID $tenantID -CSVPath $csvPath
}
I’ll let Sean answer. It’s his script and I have enough problems debugging my own code…
🙂 Thanks Tony,
Hi Cena,
That’s a simple foreach statement so it definitely shouldn’t stop after a single account. I assume the $mailboxes variable does contain the full list?
You could add some screen output to give you an idea of there it is, something like:
$mailboxes = Get-EXOMailbox -resultsize unlimited -RecipientTypeDetails usermailbox
write-host “Starting for $($mailboxes.count) mailboxes”
Foreach($mailbox in $mailboxes){
Write-host “Starting processing $($mailbox.UserPrincipalName )”
.\graph-PopulateContactsFromCSV.ps1 -Mailbox $mailbox.UserPrincipalName -ClientSecret $clientSecret -ClientID $clientID -TenantID $tenantID -CSVPath $csvPath
Write-host “Finished processing $($mailbox.UserPrincipalName )”
}