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
Figure 1: Organizational contacts created in a user mailbox

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.

About the Author

Tony Redmond

Tony Redmond has written thousands of articles about Microsoft technology since 1996. He is the lead author for the Office 365 for IT Pros eBook, the only book covering Office 365 that is updated monthly to keep pace with change in the cloud. Apart from contributing to Practical365.com, Tony also writes at Office365itpros.com to support the development of the eBook. He has been a Microsoft MVP since 2004.

Comments

  1. M365 guy

    is it possible to pull the existing (specific) contacts from user and add them to a folder automatically ?

  2. Neil

    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?

  3. Lloyd

    Hi Tony, how could we save the contacts into a designated contact folder as opposed to the default using this script?

    1. Avatar photo
      Tony Redmond

      No idea. I haven’t done any research to know how to answer that question.

  4. Lloyd

    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)”
    }
    “@

    1. Avatar photo
      Tony Redmond

      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

      1. Lloyd

        Hi Tony, that has worked! Thank you!

  5. Sascha

    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?

    1. Avatar photo
      Tony Redmond

      No idea whatsoever. Have you tried to debug the script to see what happens as each line is executed?

      1. Sascha

        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!

        1. Avatar photo
          Tony Redmond

          That line just creates a PowerShell list object. It has nothing to do with creating contacts.

  6. Max

    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.

    1. Avatar photo
      Tony Redmond

      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…

      1. Max

        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 🙂

  7. Patrick Bouaziz

    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

    1. Avatar photo
      Tony Redmond

      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

      1. Patrick BOUAZIZ

        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

        1. Avatar photo
          Tony Redmond

          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.

          1. Patrick BOUAZIZ

            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

          2. Avatar photo
            Tony Redmond

            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.

          3. Patrick BOUAZIZ

            I have followed your articles but I’m still getting the Access is denied error 🙁

          4. Avatar photo
            Tony Redmond

            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.

  8. Marin

    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!

  9. Simon

    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?

  10. Markus

    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

    1. Avatar photo
      Tony Redmond

      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?

  11. Adriano

    Hi ,
    thx for sharing the code.

    How can I try the code for a specific user ?

    1. Avatar photo
      Tony Redmond

      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”

      1. Adriano

        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?

        1. Avatar photo
          Tony Redmond

          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.

          1. Adriano

            Awesome!
            It Works fine!

            It was really this, $Mbx = Get-Mailbox -Identity $Mbx

            Thank you so much!

    2. Ben

      Hello Adriano, could you tell me which line you adjusted in the code?

      1. Ben

        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.

  12. Sameer

    Hi,
    I’m highly interested in applying the solution. Do you have a Youtube video that explains the process?

    1. Avatar photo
      Tony Redmond

      Nope. The process is fully explained in the article. I don’t see how YouTube would add any value to the discussion.

Leave a Reply