Merging Contact Groups into Lync 2013 Clients across the Enterprise

August 12, 2014

While getting ready to deploy Lync 2013 at our company, I came across two problems:

A. There is not an easy way to browse a company directory with Lync 2013 clients
2. There is not an easy way to push contact group lists to Lync 2013 clients

Create distribution groups

Although Lync can search for contacts, I wanted to allow our employees to browse a list of all co-workers with Lync 2013. However, since we have over 100 employees, and because Lync only displays distribution lists with 100 or fewer members, this is not possible. So I decided to create multiple distribution lists, each with fewer than 100 members.

First I had to discover where to split the list. It didn’t matter if there was exactly the same number in each group, but I wanted to be close.

PS C:\scripts> Import-Module ActiveDirectory
PS C:\scripts> $employees = get-aduser -filter * -searchbase "ou=employees,dc=sep,dc=local" -properties displayname | sort displayname  
PS C:\scripts> $employees[($employees.count)/2] | select displayname  
displayname  
-----------
Jennifer J. Smith  

Jennifer was in the middle, so I decided to make the split between J and K. Based on this info, I created two distribution groups in Active Directory: Employees A-J and Employees K-Z. The next step was to create a script tied to a scheduled task that daily keeps these groups up-to-date.

Put all employee accounts into distribution groups

# Get a list of all current employees (temp and fulltime) from Active Directory and separate them into two groups.
# The first group includes all employees whose first name begins with A-J.
# The second group includes all employees whose first name begins with K-Z.
Import-Module ActiveDirectory

Write-Host -foregroundcolor green "Getting current employees whose first name starts with A - J"
$employeesAJ = get-aduser -filter * -searchbase "ou=employees,dc=sep,dc=local" -properties displayname | where {$_.Displayname -match "^[a-j]"} | sort Displayname
$tempEmployeesAJ = get-aduser -filter * -searchbase "ou=temp employees,dc=sep,dc=local" -properties displayname | where {$_.Displayname -match "^[a-j]"} | sort Displayname
$usersAJ = $employeesAJ + $tempEmployeesAJ | sort displayname

Write-Host -foregroundcolor green "Getting current employees whose first name starts with K - Z"
$employeesKZ = get-aduser -filter * -searchbase "ou=employees,dc=sep,dc=local" -properties displayname | where {$_.Displayname -match "^[k-z]"} | sort Displayname
$tempEmployeesKZ = get-aduser -filter * -searchbase "ou=temp employees,dc=sep,dc=local" -properties displayname | where {$_.Displayname -match "^[k-z]"} | sort Displayname
$usersKZ = $employeesKZ + $tempEmployeesKZ | sort displayname

# Get the current members of each group for comparison
Write-Host -foregroundcolor green "Getting the members of the AD groups Employees A-J"
$groupAJ = get-adgroup "CN=Employees A-J,OU=Exchange,,DC=sep,DC=local"
$groupAJmembers = $groupAJ | get-adgroupmember

Write-Host -foregroundcolor green "Getting the members of the AD groups Employees K-Z"
$groupKZ = get-adgroup "CN=Employees K-Z,OU=Exchange,DC=sep,DC=local"
$groupKZmembers = $groupKZ | get-adgroupmember

# Add employee accounts to each group Write-Host -foregroundcolor green "Adding users to Employees A-J"
foreach ($userAJ in $usersAJ)
{
Add-ADGroupMember $groupAJ -Member $userAJ
}
Write-Host -foregroundcolor green "Adding users to Employees K-Z"
foreach ($userKZ in $usersKZ)
{
Add-ADGroupMember $groupKZ -Member $userKZ
}

Remove old accounts from the distribution groups


# Remove accounts from each group that are no longer current employees 
Write-Host -foregroundcolor green "Removing users from Employees A-J" 
foreach ($memberAJ in $groupAJmembers) 
{
     If ($usersAJ.samaccountname -notcontains $memberAJ.samaccountname)
     {
         Remove-ADPrincipalGroupMembership -Identity $memberAJ -MemberOf "Employees A-J" -Confirm:$False
         write-host -foregroundcolor red "$($memberAJ.samaccountname) has been removed"
     }
} 
Write-Host -foregroundcolor green "Removing users from Employees K-Z" 
foreach ($memberKZ in $groupKZmembers) 
{
     If ($usersKZ.samaccountname -notcontains $memberKZ.samaccountname)
    {
         Remove-ADPrincipalGroupMembership -Identity $memberKZ -MemberOf "Employees K-Z" -Confirm:$False
         write-host -foregroundcolor red "$($memberKZ.samaccountname) has been removed"
    }
}

Add contact groups to profiles

Now that the groups were populated, the next obstacle was how to push these groups out to the Lync clients. This proved more difficult than I had hoped. Although, Charles Ulrich did most of the work with his post, Lync Server 2013 – Bulk Updating Contact Groups, his method completely replaces the Lync 2013 users’ contact groups list. What I needed was a way to add groups without disturbing any of the clients’ existing groups. To do this, instead of replacing the whole ContactGroups node, I added new ContactGroup elements to the existing node. Here’s the full script:

[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") 
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$objForm = New-Object System.Windows.Forms.Form 
$objForm.Text = $prompt 
$objForm.Size = New-Object System.Drawing.Size(300,600) 
$objForm.StartPosition = "CenterScreen" 
$objForm.KeyPreview = $True 
$objForm.Add_KeyDown({if ($_.KeyCode -eq "Enter") `
    {foreach ($objItem in $objListbox.SelectedItems) `
    {$Script:x += $objItem} $objForm.Close() } }) 
$objForm.Add_KeyDown({if ($_.KeyCode -eq "Escape") `
    {$objForm.Close()}}) 
$OKButton = New-Object System.Windows.Forms.Button 
$OKButton.Location = New-Object System.Drawing.Size(75,520) 
$OKButton.Size = New-Object System.Drawing.Size(75,23) 
$OKButton.Text = "OK" 
$OKButton.Add_Click( { foreach ($objItem in $objListbox.SelectedItems) `
    {$Script:x += $objItem} $objForm.Close() }) 
$objForm.Controls.Add($OKButton) 
$CancelButton = New-Object System.Windows.Forms.Button 
$CancelButton.Location = New-Object System.Drawing.Size(150,520) 
$CancelButton.Size = New-Object System.Drawing.Size(75,23) 
$CancelButton.Text = "Cancel" 
$CancelButton.Add_Click({$objForm.Close()}) 
$objForm.Controls.Add($CancelButton) 
$objLabel = New-Object System.Windows.Forms.Label 
$objLabel.Location = New-Object System.Drawing.Size(10,20) 
$objLabel.Size = New-Object System.Drawing.Size(280,20) 
$objLabel.Text = "Please make a selection from the list below:"
$objForm.Controls.Add($objLabel) 
$objListbox = New-Object System.Windows.Forms.Listbox 
$objListbox.Location = New-Object System.Drawing.Size(10,40) 
$objListbox.Size = New-Object System.Drawing.Size(260,20) 
$objListbox.SelectionMode = $listboxtype 
$inputarray | ForEach-Object {[void] $objListbox.Items.Add($_)} 
$objListbox.Height = 470 
$objForm.Controls.Add($objListbox) 
$objForm.Topmost = $True 
$objForm.Add_Shown({$objForm.Activate()}) 
[void] $objForm.ShowDialog() 
Return $Script:x 

#Load Lync Powershell Commands 
Import-Module Lync 
#Who should we update 
$sora = Read-Host "Do you want to update (A)ll users or a (S)ingle user? (S or A)" 
If ($sora -eq "S") 
{
     # Get user to update
     $userlist = Get-CSUser
     $user_email=MultipleSelectionBox
     $userlist.UserPrincipalName "Choose Lync 2013 User" "One"
     #Export Single Users Data 
     Export-CsUserData -PoolFqdn
     $lyncpoolfqdn -UserFilter $user_email -FileName $ExportFileNamePath 
} 
Else 
{
     #Export All Users Data
     Export-CsUserData -PoolFqdn $lyncpoolfqdn -FileName $ExportFileNamePath 
} 
#Extract the Exported Zip file. Requires 7-Zip 
Write-Host " " &$7ZipPath e $ExportFileNamePath $7ZipOutputParam 
Write-Host " " 
Write-Host " " 
$original = [System.Xml.XmlDocument](Get-Content $LyncXMLFile) 
#Set our loop counter to 0 
$count = $original.DocItemSet.DocItem.Count + 1 
Write-Host "########################################################################" 
# look for the groups with the following DisplayNames 
$newGroup1 = "RW1wbG95ZWVzIEEtSg==" 
$newGroup2 = "RW1wbG95ZWVzIEstWg==" 
#$newGroup3 = "" 
#Loop through all DocItem Elements and add ContactGroups in them 
For ($i=0; $i -lt $count; $i++) 
{
     If (($original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count -gt 0))
     {
         #number of contact groups
         $ContactGroupCount = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count
         # get the target node
         Write-Host " "
         Write-Host "Working on XML Node: "
         $original.DocItemSet.DocItem[$i].Name
         Write-Host " "
         Write-Host "Contact Groups Before: "
         $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count
         Write-Host " "
         $inner = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups
         # the default expectation is that the groups are not present
         $Group1present = $false
         $Group2present = $false
         # $Group3present = $false
         # check if the new groups are already present
         For ($j=0; $j -lt $ContactGroupCount; $j++)
         {
             $group = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup[$j]
             If ($newGroup1 -eq ($group | select -ExpandProperty DisplayName))
             { $Group1present = $true}
             If ($newGroup2 -eq ($group | select -ExpandProperty DisplayName))
             { $Group2present = $true }
             # If ($newGroup3 -eq ($group | select -ExpandProperty DisplayName))
             { $Group3present = $true }
         }
         #Add groups to current contact group list if they are missing
         If ($Group1present -eq $false)
         {
             $ContactGroupCount++
             $newXmlElement = $original.CreateElement("ContactGroup")
             $newXml = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.AppendChild($newXmlElement)
             $newXml.SetAttribute("Number","$ContactGroupCount")
             $newXml.SetAttribute("DisplayName", "RW1wbG95ZWVzIEEtSg==")
             $newXml.SetAttribute("ExternalUri", "PGdyb3VwRXh0ZW5zaW9uIGdyb3VwVHlwZT0iZGciPjxlbWFpbD5lbXBsb3llZXNhLWpAc2VwLmNvbTwvZW1haWw+PC9ncm91cEV4dGVuc2lvbj4=")
             Write-Host -foregroundcolor green $newXml
         }
         If ($Group2present -eq $false)
         {
             $ContactGroupCount++ $newXmlElement = $original.CreateElement("ContactGroup")
             $newXml = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.AppendChild($newXmlElement)
             $newXml.SetAttribute("Number","$ContactGroupCount")
             $newXml.SetAttribute("DisplayName", "RW1wbG95ZWVzIEstWg==")
             $newXml.SetAttribute("ExternalUri", "PGdyb3VwRXh0ZW5zaW9uIGdyb3VwVHlwZT0iZGciPjxlbWFpbD5lbXBsb3llZXNrLXpAc2VwLmNvbTwvZW1haWw+PC9ncm91cEV4dGVuc2lvbj4=")
             Write-Host -foregroundcolor red $newXml
         }
         # If ($Group3present -eq $false)
         # {
         # $ContactGroupCount++
         # $newXmlElement = $original.CreateElement("ContactGroup")
         # $newXml = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.AppendChild($newXmlElement)
         # $newXml.SetAttribute("Number","$ContactGroupCount")
         # $newXml.SetAttribute("DisplayName", "")
         # $newXml.SetAttribute("ExternalUri", "")
         # Write-Host -foregroundcolor red $newXml
         # }
         Write-Host "Contact Groups After: "
         $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count
     }
} 
Write-Host " " 
Write-Host "########################################################################"
Write-Host " " 
#Remove blank xmlns tags created by importing the node 
$original = $original.OuterXml.Replace(" xmlns=`"`"", "") 
# save changes (full path to file) 
$original.Save($LyncXMLFile) 
# create updated zip file & $7ZipPath a $UpdatedFileNamePath $7ZipIncludeFiles 
Write-Host " " 
Write-Host "########################################################################" 
Write-Host " " 
Write-Host "The XML file has been updated with the default groups." 
Write-Host " " 
Write-Host "If you want to take a look at the file its path is" 
Write-Host " " 
Write-Host $LyncXMLFile 
Write-Host " " 
Write-Host "This file will be deleted once this script has finished." 
Write-Host " " 
Write-Host "########################################################################" 
Write-Host " " 
$sure = Read-Host "About to upload the changes to the server. Are you sure? (Y or N)" 
If ($sure -eq "Y") 
{
     Write-Host " "
     Write-Host "Updating Server with the new Contact Group Settings."
     Write-Host " "
     Write-Host "The user(s) will have no Contact groups until they"
     Write-Host "log off and back on to Lync."
     If ($sora -eq "S")
     {
         # Update the server with the new User Data
         Update-CsUserData -Filename $UpdatedFileNamePath -UserFilter 
         $user_email
     }
     Else
     {
         # Update the server with the new User Data
         Update-CsUserData -Filename $UpdatedFileNamePath
     }
     Write-Host " " 
} 
Else 
{
     Write-Host " "
     Write-Host "Update Aborted!!!!!!"
     Write-Host " "
     Write-Host "Please Come Again! :) "
     Write-Host " " 
} 
Write-Host "I will clean up the files and close the window after you hit Enter." 
Write-Host " " PAUSE 
# Clean Up 
Remove-Item ($BaseFilePath + "\*") -recurse

 Customize the script

To use this script for yourself make the following changes.

  1. Export your Lync profile (Export-CsUserData -PoolFqdn $lyncpoolfqdn -UserFilter $user_email -FileName $ExportFileNamePath).
  2. Add some new groups to your contact groups list in your Lync client.
  3. Export your profile again to a different file, extract the zip files and compare the two DocItemSet.xml files. Copy to Notepad the new ContactGroup elements.
  4. In the script, set the variables $newGroup1, $newGroup2, etc. to the DisplayNames attributes of the elements you need to add. (If you only need to add one element, comment out the unnecessary variables).
  5. In the section “#Add groups to current contact group list if they are missing”, replace the DisplayName and ExternalUri attributes. Comment out any unnecessary If statement blocks.

You now have the ability to merge new contact groups into Lync 2013 clients across the enterprise.