Unified Contacts Store (UCS) Deep Dive

I decided to make a bit of a deep dive inside the UCS (Unified Contact Store) in Lync/Skype for Business.

There are two options to store your contacts – in the Lync/SfB SQL store or in the Exchange mailbox.

For me the SQL store is the less problematic one, migrating users between different pools and Office 365 is less likely to cause a problems.

Option 1: Store in Lync/SfB SQL

SELECT UserAtHost, UpdateTime, InsertTime, ExUmEnabled, UcsMode, UcsMigrationAttemptCount, LastUcsMigrationAttempt FROM [rtc].[dbo].[Resource] as r
    INNER JOIN [rtc].[dbo].[ResourceDirectory] as d on (d.ResourceId = r.ResourceId)
    INNER JOIN [rtc].[dbo].[PresenceHomedResource] as h on (h.ResourceId = d.ResourceId)
--- Optional where clause to check specific users
--- WHERE r.UserAtHost IN ( 'user@domain.com', 'user2@domain.com' )

Option 2: Unified Contact Store in Exchange

MrMAPI.exe -ChildFolders -Folder "IPM_SUBTREE\Contacts"

MrMAPI.exe -Folder "IPM_SUBTREE\Contacts\{A9E2BC46-B3A0-4243-B315-60D991004455}"

Second-hop PowerShell remoting with Skype for Business/Lync cmdlets

I have faced a huge issue trying to run mutliple commands via remote PowerShell on different Front End servers, but apparently if the cmdlet needs to access resources from the Back End database – the command fails with this:

Active Directory error "-2147016672" occurred while searching for domain controllers

which is a very frustrating error.

For example running Get-CsCertificate will work, as the information is saved locally on the Front End, however Test-CsDatabase -LocalService will fail with this error, because executing this from your machine (in my case in a different domain) first jumps on the Front End, then requires a token from the Front End’s AD to send to the 3rd server for authentication – the SQL Back End where the connection breaks.

I also experimented with Get-CsConnections.ps1 script, and got the same problem.

So I came up with a bit ugly, but working solution for this. It is simple – Scheduled tasks.

$trigger = New-ScheduledTaskTrigger -AtStartup
$action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument 'Import-Module SkypeForBusiness; Test-CsDatabase -LocalService | convertto-csv -notypeinformation | out-file C:\temp\db.txt' 
$principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\NETWORK SERVICE" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal
Register-ScheduledTask Test-CsDatabaseOnce -InputObject $task
Start-ScheduledTask -TaskName Test-CsDatabaseOnce
do {
    $state = (Get-ScheduledTask -TaskName Test-CsDatabaseOnce).State
    Start-Sleep -Milliseconds 200
} while ($state -eq "Running")
Get-Content C:\temp\db.txt | select -skip 1
Unregister-ScheduledTask -TaskName Test-CsDatabaseOnce -Confirm:$false

Here I create a simple task with the Network Service account and the name Test-CsDatabaseOnce, then run it, wait for the task to complete, which exports my results in csv in C:\temp\db.txt, and finally I get my results and work with them further.

Did the same thing with Get-CsConnections.ps1, however before that I had to deploy the script locally on each Front End with the simple code below:

$local_file = "C:\temp\Get-CsConnections.ps1"
$content = [system.io.file]::ReadAllBytes($local_file)
$remote_file = "C:\temp\Get-CsConnections.ps1"
Invoke-Command -Session $session -ScriptBlock { Param($path) [system.IO.file]::WriteAllBytes($path,$using:content) } -ArgumentList $remote_file

PowerShell get variable names from PSCustomObject

If you for example import a .csv file and loop trough it with with PowerShell, each entry is from the type PSCustomObject.

Recently I wanted to see an easy way to get the column names from the .csv file as well as the values, so I found out this very easy solution:

$csv | ForEach-Object {
 $row = $_;
 #give an array of columns for this item
 $columns = ($row.PSObject.Properties.Name);
 #gives an array of the values for this item
 $data = $row.psobject.Properties.Value;
}

I also wanted to do a comma separated list of the values, which I did with the following snippet:

 $commas_columns = $columns -join ",";
 $commas_data = "'{0}'" -f ($data -join "','");

Office 365 License Management for Skype for Business Online

So I had a customer recently that wanted to update licenses on multiple users but only for Skype for Business Online. As I was researching I found out that in the web panel there is no option to that in bulk. Also there is no easy option to do in the PowerShell too. So I had to come out with a simple and fast solution so that I could enable all users only for Skype for Business Online at once. Another special requirement was to ONLY enable the Skype for Business service and ONLY for users having the E3 license assigned.

As you might know, Office 365 License has multiple services in it, so you can have an E3 user with Skype enabled and another one with Skype disabled. Gets trickier because there is not option to just ‘enable’ or ‘disable’ a service for a user (as you have the option in the admin panel clicking with your mouse user after user…). The only provided way with PowerShell is to create license options, per license, specifying which services should be disabled, by default all remaining services are deemed as enabled.

First you need to get the account sku with the following command:

Get-MsolAccountSku

You will receive a table with fist collumn AccountSkuId, you need the text before the colon, so for example mytennant365:SOME_SERVICE.

I will explain how the script works first, then you can review it yourself below.

  1. I have a .csv file called files.csv with only 1 column, with the name WindowsEmailAddress, containing the SMTP addresses of the users that need to be updated
  2. Loop trough all users and and get the licenses assigned (you can have multiple licenses assigned to a user!)
  3. Loop trough all licenses and only filter the E3 one (ENTERPRISEPACK) and get all services with their status (being disabled, enabled, etc.)
  4. Create an empty array of disabled services (options)
  5. Loop trough all services and copy only the disabled plans to the new empty array with disabled options, but SKIP the Skype for Business Online one (MCOSTANDARD).
  6. Create a service license option with the disabled services (skipping the Skype for Business Online one)
  7. Assign the newly created license options to the user
$users = Import-Csv file.csv
$users | ForEach-Object {
	$upn = $_.WindowsEmailAddress;
	$l = (Get-MsolUser -UserPrincipalName $upn).Licenses;
	$l | ForEach-Object {
		if ($_.AccountSkuId -eq "mytennant365:ENTERPRISEPACK"){
			$p = $_.ServiceStatus;
			$DisabledOptions = @()
			$p | ForEach-Object {
				if ($_.ProvisioningStatus -eq "Disabled" -and $_.ServicePlan.ServiceName -ne "MCOSTANDARD"){
					$DisabledOptions += $_.ServicePlan.ServiceName
				}
			}
			$x = New-MsolLicenseOptions -AccountSkuId "mytennant365:ENTERPRISEPACK" -DisabledPlans $DisabledOptions
			Set-MsolUserLicense -UserPrincipalName $upn -LicenseOptions $x;
		}
	}
}

The WinRM client cannot process the request

When trying to login to Office 365 with New-PSSession you might reiceve the following error:

New-PSSession : [admin1e.online.lync.com] Connecting to remote server admin1e.online.lync.com failed with the following error message : The WinRM client cannot process the request. Basic authentication is currently disabled in the client configuration. Change the
client configuration and try the request again.

So I created a small cmd snippet that resolves this (you need local administrator rights to do it):

reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client /t REG_DWORD /v AllowBasic /d 1 /f
reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service /t REG_DWORD /v AllowBasic /d 1 /f
winrm set winrm/config/service/auth @{Basic="true"}

Lync / Skype for Business client contacts export/import tool

I had a lot of issues with UCS on some of my customers so I had to come out with a solution for exporting and importing the contacts of the user from their client.

I used the SDK to develop 2 small applications, export.exe and import.exe. When you run the export.exe you get a BuddyList.xml in the same folder (you should be able to have write permissions there).

Running import.exe must be done with BuddyList.xml in the same folder, or it will fail.

The scripts are based on this PowerShell implementation, but with some bonuses:

  1. Does not require
    • Deployment of .dll file
    • PowerShell to be installed
    • Unrestricted execution rights for PowerShell scripts
    • Administrator access
  2. Does not duplicate contacts
  3. Does not duplicate groups
  4. The script takes into consideration the type of groups that you have:
    • DistributionGroup
    • FavoriteContacts/FrequentContacts
    • Custom created groups
  5. For distribution groups a lookup is first done to add it as a DG, not a custom group

You can download them from my OneDrive here: https://1drv.ms/f/s!ApaF5HY0Od2UhC861WCU5rqiTzum

Keep in mind that downloading random .exe files on the internet is dangerous!

If you would like to get the source code, I can provide it to you, just contact me on my email.

Export Contacts from Exchange 2013 (with Impersonate rights)

So this is how I managed to export contacts SMTP addresses from a exchange 2013 mailbox using an admin user with impersonate rights:

#admin should have impersonate rights
$admin = "admin@domain.com"
$user = "user@domain.com"
#First Find Microsoft.Exchange.WebServices.dll location
$dllpath = "D:\Exchange Server\Bin\Microsoft.Exchange.WebServices.dll"
Import-Module $dllpath
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)
#admin account credentials
$psCred = Get-Credential
$creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(), $psCred.GetNetworkCredential().password.ToString())
$service.Credentials = $creds
$service.AutodiscoverUrl($admin ,{$true})
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$user);
$contactsFolder = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Contacts,$user)
$contacts = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$contactsFolder)
$view = new-object Microsoft.Exchange.WebServices.Data.ItemView($contacts.TotalCount, 0)
$results = $contacts.FindItems($view)
$response = $service.LoadPropertiesForItems($results, [Microsoft.Exchange.WebServices.Data.PropertySet]::FirstClassProperties)
foreach ($item in $results){
echo $item.Item.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1].Address
}

A simple agent to convert log.nsf file to plain text file

You should also have a view, called LTT with no categorisation and showing all log entries you need exported.​
Sub Initialize
	Dim session As New NotesSession
	Dim db As NotesDatabase
	Dim view As NotesView
	Dim doc As NotesDocument
	Dim fileNum As Integer
	Set db = session.Currentdatabase
	Set view = db.Getview("LTT")
	Set doc = view.GetFirstDocument
	fileNum% = FreeFile()
	Open "C:\log.txt" For Append As fileNum% 	
	While Not ( doc Is Nothing )
		ForAll v In doc.EventList
			Print #fileNum% , v
		End ForAll
		Set doc = view.GetNextDocument(doc)
	Wend
	Close fileNum%​
End Sub​
Data is exported to C:\log.txt

A regex to match Domino FQDN

​A simple regex that can match a Domino FQDN:

([ A-Za-z0-9._%+-]+)(\/\b[A-Za-z0-9._%+-]+)+

First matching group​ ([ A-Za-z0-9._%+-]+) captures names with only first, first + last or first + middle + last names.

The second matching group (\/\b[A-Za-z0-9._%+-]+)+ captures a space-free “/Org” pattern multiple times.

Non-persistent keyboard layouts

I found out that my keyboard layouts were not staying in VDI after I log off. So I had to automate this and not lose my time with adding my layouts every day.

So you simply create a .cmd file with the following content:

control intl.cpl,, /f:”DRIVE:\FULL\PATH\TO\FILE.XML”​

Then you create a FILE.XML in that location, containing the folowing XML:

<gs:GlobalizationServices xmlns:gs="urn:longhornGlobalizationUnattend">
    <gs:UserList>
        <gs:User UserID="Current" CopySettingsToDefaultUserAcct="true" CopySettingsToSystemAcct="true"/></gs:UserList>
    <gs:InputPreferences>
        <gs:InputLanguageID Action="add" ID="0402:00040402"/>
    ​</gs:InputPreferences>
</gs:GlobalizationServices>

​The 00040402​ is the hex code identifier of the keyboard layout you need (in this case 0x00040402 – Bulgarian (phonetic traditional).

This article has all the keyboard hexadecimal identifiers that one might need:

https://technet.microsoft.com/en-us/library/hh825684.aspx​

Then you just open the windows scheduler and add this .cmd to execute on Log On and on Work station Unlock and add to Window Startup folder and there you go!