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:

Prepopulating Outlook Contacts with the Graph API
Figure 1: Add the Contacts.ReadWrite permission and grant admin consent.

Moving back to the app properties, note the Application (client) ID and Directory (tenant) ID from the Overview page:

Prepopulating Outlook Contacts with the Graph API
Figure 2: Take note of the Application and Directory ID values.

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:

Prepopulating Outlook Contacts with the Graph API
Figure 3: The sample file is available from GitHub.

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:

Prepopulating Outlook Contacts with the Graph API
Figure 4: Running the script for one user.

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:

Prepopulating Outlook Contacts with the Graph API
Figure 5: The contact is provisioned into the user’s personal contacts.

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.

About the Author

Sean McAvinue

Sean McAvinue is a Microsoft MVP in Office Development and has been working with Microsoft Technologies for more than 10 years. As Modern Workplace Practice Lead at Ergo Group, he helps customers with planning, deploying and maximizing the many benefits of Microsoft 365 with a focus on security and automation. With a passion for creative problem solving, he enjoys developing solutions for business requirements by leveraging new technologies or by extending the built-in functionality with automation. Blogs frequently at https://seanmcavinue.net and loves sharing and collaborating with the community. To reach out to Sean, you can find him on Twitter at @sean_mcavinue

Comments

  1. Cena

    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.

  2. KJ

    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?

    1. Sean McAvinue

      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

  3. sam

    Can I import bulk outlook contacts using JSON format

    1. Sean McAvinue

      Yes you can, JSON is the format the code builds but with some modifications you could use JSON as in input file.

  4. michael

    I am still running Exchange on-prem, is there a way to make this script run with Exchange On-Prem?

    1. Sean McAvinue

      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.

  5. LukasF

    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. —>

    1. Avatar photo
      Tony Redmond

      I don’t know. I never tried importing with an XML file.

  6. Cena

    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.

  7. Bimal

    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.

  8. Miro

    When using this script, contacts are not updated, rather it creates duplicates. That is bad for us.

    1. Sean McAvinue

      That is expected,
      The script could be modified to check for existing contacts if you require to update existing ones.

      1. XIm

        Hi Sean,
        How to check for the existing contacts and not create a duplicate for it?

  9. G

    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

    1. Sean Mcavinue

      Hi G,
      Looks like you are most likely missing the Contact.readwrite permission – make sure it is an Application permission, not delegated.

      1. G

        Thank you so much Sean, what a stupid mistake on my side! Ofc it worked as a charm!

  10. Patrick

    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.

  11. KHIREDDINE

    Hey,
    Thank you for this job, my question :
    is there a way to automate this directly from AD ?

    1. Sean Mcavinue

      You could always modify the script to import from AD as a source

      1. Petr

        Would you have any more details on this one? a brief “how to” perhaps? I would be very, very thankful!! Cheers!

        1. Sean McAvinue

          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.

  12. Adriano

    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

    1. ahmed

      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.

      1. Sean McAvinue

        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?

        1. João Ribeiro

          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.

          1. João Ribeiro

            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,…

      2. Sean McAvinue

        A 400 error would imply that the permissions on the App Reg aren’t correct – make sure you have added them as Application Permissions

    2. G

      Hi Adriano,
      I am having the same issue did you fixed it?

  13. Michele

    what does Error obtaining Token mean? I am sure that application’s registration steps has been done properly.
    thanks

    1. michele

      graph-PopulateContactsFromCSV.ps1
      :205 char:5

      1. Avatar photo
        Sean McAvinue

        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.

  14. Florian

    Hi, great script. Thx!
    How do you deal with changed users (e.g. marriage or address change)?

  15. Alex

    Hello,
    would it be possible to use contacts from a o365 account contacts folder instead of a csv?

  16. Louis

    Does this script need to be run every time a new user is added?

      1. michele

        hey Tony, do you know how to import contacts avoid contact’s duplication?
        thanks
        Michele

      2. Cena

        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?

          1. Cena

            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!!

          2. Avatar photo
            Tony Redmond

            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()) }

      3. Cena

        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
        }

        1. Avatar photo
          Tony Redmond

          I’ll let Sean answer. It’s his script and I have enough problems debugging my own code…

          1. Sean McAvinue

            🙂 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 )”
            }

Leave a Reply