If you’ve ever looked at configuring the Exchange Availability Service to allow cross-forest free/busy lookups you’ve probably realised that the documentation surrounding it is awful.

Get-MailboxServer | Add-ADPermission -Accessrights Extendedright -Extendedrights "ms-Exch-EPI-Token-Serialization" -User "<Remote Forest Domain>\Exchange servers"

From here, doesn’t even work for a start, because Get-MailboxServer doesn’t return the correct identity objects for Add-ADPermission. Once you’ve worked out how to get that sorted and done your

Add-AvailabilityAddressSpace -Forestname ContosoForest.com -AccessMethod PerUserFB -UseServiceAccount:$true

You’re probably thinking that you’re done, but it usually isn’t that simple. For a start if the namespaces in either forest are even a little bit complicated or you’re using a custom target address space then you’re in for some autodiscover fun – just because autodiscover works in its home forest doesn’t mean it will from your trusted partner. Before you even start, make sure your Exchange certificates are trusted by the target servers; don’t assume that just because they’re from a public CA that they will be, you never know what weird stuff has been done to the servers if you didn’t build them and they’re not under your control. Obviously if you can you should export the SCP to the partner forest with

Export-AutodiscoverConfig -TargetForestDomainController "dc.contoso.com" -TargetForestCredential (Get-Credential) -MultipleExchangeDeployments $true

But even then there are a few things that aren’t clearly documented and might trip you up; for example, you need Outlook Anywhere enabled for the Availability Service to function, which isn’t a given if you’re still running Exchange 2010 or 2007 in one or both forests. Furthermore, if one or both parties are running Exchange 2013 or 2016 and you’re not using the SCP for autodiscover then you’ll probably find the free/busy lookups fail because:

the availability service sends an Autodiscover request by using an automatically generated SMTP address for the anchor mailbox. This SMTP address that is used is 01B62C6D-4324-448f-9884-5FEC6D18A7E2@Availability_Address_Space_domain.

However, the Exchange Server 2013 Client Access server in the attendee forest cannot locate a mailbox for this email address and responds with a 404 status.

That’s right, Exchange uses an essentially random (and as far as I can tell only documented in that KB article) SMTP address for it’s autodiscover query which is rejected by the target server because, obviously, it doesn’t exist. The “fix”? Slap that SMTP address onto any old mailbox so the server returns a valid autodiscover response.

Hopefully this post will be of some help to anyone struggling to get the availability service working in their environment, I spent 2 weeks dicking about with Microsoft support trying to understand how it operates so that with any luck you won’t have to.



You know how annoying it is when you return some information in Powershell that includes a list of items and the console helpfully truncates it with a …

 

 

 

Whereas what you really want is for it to just show the whole thing like:

 

 

 

Well you can. The truncation is controlled by $FormatEnumerationLimit and if you set it to -1 it won’t truncate output at all. The default for a standard Powershell instance is 4, the Exchange Management Shell ups this to 16 and other console files may make their own modifications.

Simple.



We all know how annoying it is working somewhere with a proxy server that requires authentication, especially as Microsoft increasingly don’t support the scenario with many of their Azure-related tools. However, it is quite possible to use authenticated proxies with .NET applications including Powershell.

For the former, edit the application .config file and add

<system.net>
<defaultProxy useDefaultCredentials="true" />
</system.net>

And for Powershell, add the following to your scripts or $profile

$proxyString = "http://proxy:8080"
$proxyUri = new-object System.Uri($proxyString)
 
[System.Net.WebRequest]::DefaultWebProxy = new-object System.Net.WebProxy ($proxyUri, $true)
[System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials


Lync and Skype for Business offer Call Detail Recording, essentially just a record of all calls to, from and within your Lync/Skype infrastructure. Only problem is it’s a pain to query. Now there are some SSRS custom reports available as well as the default templates but sometimes you just want to get in there and pull data quickly from SQL.

This, for example, will get all audio calls from the last 10 days:

SELECT [SessionIdTime],[ResponseTime],[SessionEndTime],DATEDIFF(ss,[ResponseTime],[SessionEndTime]) AS Duration,u1.[UserUri] AS User1Uri,[User1Id],u2.[UserUri] AS User2Uri,[User2Id],[TargetUserId],[SessionStartedById],[OnBehalfOfId],[ReferredById] FROM [LcsCDR].[dbo].[SessionDetails] s LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u1 ON s.[User1Id] = u1.[UserId] LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u2 ON s.[User2Id] = u2.[UserId] WHERE [User1Id] != [User2Id] AND [MediaTypes] = 16 AND [ResponseTime] >= dateadd(dd,0, datediff(dd,10, getDate())) ORDER BY [ResponseTime] DESC

Change the number value here: datediff(dd,10, getDate())) to display a different number of days worth of calls. Change the value of [MediaTypes] to alter the call type you’re searching for as per the database schema.

These examples will get you the user ID for a given SIP URI or external number:

SELECT [UserId] FROM [LcsCDR].[dbo].[Users] WHERE [UserUri] LIKE '+441212001234%'
SELECT [UserId] FROM [LcsCDR].[dbo].[Users] WHERE [UserUri] LIKE 'jimbob@example.com%'

With those UserIDs you can filter calls to a user (in this case user ID 147):

SELECT [SessionIdTime],[ResponseTime],[SessionEndTime],DATEDIFF(ss,[ResponseTime],[SessionEndTime]) AS Duration,u1.[UserUri] AS User1Uri,[User1Id],u2.[UserUri] AS User2Uri,[User2Id],[TargetUserId],[SessionStartedById],[OnBehalfOfId],[ReferredById] FROM [LcsCDR].[dbo].[SessionDetails] s LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u1 ON s.[User1Id] = u1.[UserId] LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u2 ON s.[User2Id] = u2.[UserId] WHERE [User1Id] != [User2Id] AND [MediaTypes] = 16 AND [User2Id] = '147' ORDER BY [ResponseTime] DESC

Or from a user:

SELECT [SessionIdTime],[ResponseTime],[SessionEndTime],DATEDIFF(ss,[ResponseTime],[SessionEndTime]) AS Duration,u1.[UserUri] AS User1Uri,[User1Id],u2.[UserUri] AS User2Uri,[User2Id],[TargetUserId],[SessionStartedById],[OnBehalfOfId],[ReferredById] FROM [LcsCDR].[dbo].[SessionDetails] s LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u1 ON s.[User1Id] = u1.[UserId] LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u2 ON s.[User2Id] = u2.[UserId] WHERE [User1Id] != [User2Id] AND [MediaTypes] = 16 AND [User1Id] = '147' ORDER BY [ResponseTime] DESC

Or both:

SELECT [SessionIdTime],[ResponseTime],[SessionEndTime],DATEDIFF(ss,[ResponseTime],[SessionEndTime]) AS Duration,u1.[UserUri] AS User1Uri,[User1Id],u2.[UserUri] AS User2Uri,[User2Id],[TargetUserId],[SessionStartedById],[OnBehalfOfId],[ReferredById] FROM [LcsCDR].[dbo].[SessionDetails] s LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u1 ON s.[User1Id] = u1.[UserId] LEFT OUTER JOIN [LcsCDR].[dbo].[Users] u2 ON s.[User2Id] = u2.[UserId] WHERE [User1Id] != [User2Id] AND [MediaTypes] = 16 AND ([User1Id] = '147' OR [User2Id] = '147') ORDER BY [ResponseTime] DESC

You can of course filter on [ResponseTime] or even [InviteTime] to limit your query by timeframe similarly to the first example. If you have a nose through the SessionDetails table in the database you’ll be able to see all the additional columns that you can also query but IMO the ones I’ve included tend to be the important ones – who the call is two/from, when it was made, how long it was and whether it was made on behalf of or referred by another user. Some columns you may find useful are [IsUser1Internal] and [IsUser2Internal] which will tell you if one or both parties were connecting via one of your Edge servers rather than internally and [ResponseCode] which will tell you whether or not the call was successfull (a “200” code means success).



This script uses UCWA to send IMs via Skype For Business. Unlike the commonly documented methods using Microsoft.Lync.Model from the Lync SDK which require the Lync/S4B client to be installed, running and logged in to work, this will run from any machine on your internal network (and in theory could be used externally with some tweaking too). It still needs some cleaning up because none of the code examples I could find were for Powershell so there was a bit of trial and error and I’m sure things can be done more efficiently, but at a basic level it does what it’s supposed to.

The help should cover most of the arguments, it’s all fairly self-explanatory. You’ll probably want to update the following line in the script with your own Application name and GUID:

$postparams = @{UserAgent="My UCWA Application";EndpointId="75d0449f-aa09-4f5d-add5-eeefc518c48a";Culture="en-US"} | ConvertTo-JSON

You can use ([guid]::NewGuid()).guid to generate a new GUID for your application.

You can also edit the default $messageheader, $messagefooter, $messagebody & $messagesubject values. The Subject and Header must be plaintext, the Body and Footer support HTML.

#Requires -version 4
 
<#
.SYNOPSIS
	Sends IM via S4B
.DESCRIPTION
    Sends IM via S4B using UCWA
.PARAMETER username
	Username of account used to send messages in UPN format
.PARAMETER password
	Password of account as plaintext (exclusive of pwd)
.PARAMETER pwd
	Password of account as securestring (exclusive of password)
.PARAMETER recipients
	List of recipient SIP addresses, comma separated
.PARAMETER messagesubject
	Message subject (Plain text only)
.PARAMETER messagebody
	Message body (HTML allowed)
.PARAMETER messageheader
	Message header (Plain text only), sent as part of the invitation
.PARAMETER messagefooter
	Message footer (HTML allowed), sent after the message body
.EXAMPLE
    C:\>Send-S4BMessage.ps1
.NOTES
    Author: Adam Beardwood
    Date: January 13, 2016
    v1.0: Initial Release
    v1.1: Bugfix for multiple recipients
#>
 
[CmdletBinding(DefaultParameterSetName="secure")]
param(
	[Parameter(Mandatory=$true)][string]$username,
	[Parameter(Mandatory=$true,ParameterSetName='secure')][System.Security.SecureString]$pwd,
	[Parameter(Mandatory=$true,ParameterSetName='plain')][string]$password,
	[Parameter(Mandatory=$true)][array]$recipients,
	[Parameter(Mandatory=$false)][string]$messagesubject,
	[Parameter(Mandatory=$false)][string]$messagebody,
	[Parameter(Mandatory=$false)][string]$messageheader,
	[Parameter(Mandatory=$false)][string]$messagefooter
)
 
function sendmessage ($operationID, $rootappurl, $appid, $authcwt, $recipient, $messagesubject, $messagebody, $messagefooter, $ackid) {
 
	$statecount = 0
	$ackid = $ackid.tostring()
 
	write-verbose "#Send Message Invite"
 
	write-verbose "$rootappurl/$appid/communication/"
 
	try{
		$postparams = @{"importance"="Normal";"sessionContext"="$(([guid]::NewGuid()).guid)";"subject"="$messagesubject";"telemetryId"=$null;"to"=$recipient;"operationId"=$operationID;"_links"=@{"message"=@{"href"="data:text/plain,$messageheader"}}} | convertto-json
 
		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/communication/messagingInvitations" -Method POST -Body "$postparams" -Headers @{"Authorization"="Bearer $authcwt"} -ContentType "application/json" -UseBasicParsing
	}catch{
		write-verbose "Unable to send message invite"
		return $false
	}
 
	write-verbose "#Check state & get Conversation ID"
 
	do{
 
		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/events?ack=$ackid" -Method GET -Headers @{"Authorization"="Bearer $authcwt"} -ContentType "application/JSON" -UseBasicParsing
 
		$state = (($data.content | ConvertFrom-JSON).sender.events._embedded.messaging.state)
 
		if($state.gettype().name -eq "string"){
 
		}else{
			$state = $state[-1]
		}
 
		$statecount++
 
		if(($statecount -ge 25) -or ($state -eq "Disconnected")){
			write-verbose "No response from endpoint or conversation declined"
			return $false
		}
 
		start-sleep 1
 
	}while($state -notcontains "Connected")
 
	$JSONdata = $data.content | ConvertFrom-JSON 
 
	$conversationID = ($JSONdata.sender.events.link | ?{$_.rel -eq "conversation"}).href.split("/")[-1]
 
	$conversationID
 
	write-verbose "#Send Messages"
 
	try{
		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/communication/conversations/$conversationID/messaging/messages" -Method POST -Body $messagebody -Headers @{"Authorization"="Bearer $authcwt"} -ContentType "text/HTML" -UseBasicParsing
		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/communication/conversations/$conversationID/messaging/messages" -Method POST -Body $messagefooter -Headers @{"Authorization"="Bearer $authcwt"} -ContentType "text/HTML" -UseBasicParsing
	}catch{
		write-verbose "Unable to send message"
		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/communication/conversations/$conversationID/messaging/terminate" -Method POST -Headers @{"Authorization"="Bearer $authcwt"} -UseBasicParsing
		return $false
	}
	write-verbose "#Terminate Conversation"
 
	try{
		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/communication/conversations/$conversationID/messaging/terminate" -Method POST -Headers @{"Authorization"="Bearer $authcwt"} -UseBasicParsing
	}catch{
		write-verbose "Failed to terminate conversation"
		return $false
	}
 
	rv conversationID, recipient, ackid, state
 
	return $true
 
}
 
if($pwd){[string]$password = (New-Object System.Management.Automation.PSCredential('dummy',$pwd)).getnetworkcredential().password}
if(!$messagesubject){$messagesubject = "S4B Automated Message"}
if(!$messagebody){$messagebody = "<font face='calibri'>Someone forgot to set a message so you're seeing this default instead</font>"}
if(!$messageheader){$messageheader = "This is an automated alert from <system>"}
if(!$messagefooter){$messagefooter = "<font face='calibri'><i>This message was sent by a bot, there's no point in replying to it.</i></font>"}
 
write-verbose "#Get Autodiscover Information"
 
try{
	$data = Invoke-WebRequest -Uri "https://lyncdiscoverinternal.$($env:userdnsdomain)" -Method GET -ContentType "application/json" -UseBasicParsing
 
	$baseurl = (($data.content | ConvertFrom-JSON)._links.user.href).split("/")[0..2] -join "/"
 
	$oauthurl = ($data.content | convertfrom-json)._links.user.href
}catch{
	write-output "Unable to get autodiscover information"
	exit 1
}
write-verbose "#Authenticate to server"
 
try{
	$postParams = @{grant_type="password";username=$username;password=$password}
 
	$data = Invoke-WebRequest -Uri "$baseurl/WebTicket/oauthtoken" -Method POST -Body $postParams -UseBasicParsing
 
	$authcwt = ($data.content | ConvertFrom-JSON).access_token
}catch{
	write-output "Unable to authenticate, verify credentials and try again"
	exit 1
}
write-verbose "#Get application URLs"
 
try{
	$data = Invoke-WebRequest -Uri "$oauthurl" -Method GET -Headers @{"Authorization"="Bearer $authcwt"} -UseBasicParsing
 
	$rootappurl = ($data.content | ConvertFrom-JSON)._links.applications.href
}catch{
	write-output "Unable to get Application URLs"
	exit 1
}
 
write-verbose "#Create App Instance"
 
try{
	$postparams = @{UserAgent="My UCWA Application";EndpointId="75d0449f-aa09-4f5d-add5-eeefc518c48a";Culture="en-US"} | ConvertTo-JSON
 
	$data = Invoke-WebRequest -Uri "$rootappurl" -Method POST -Body "$postparams" -Headers @{"Authorization"="Bearer $authcwt"} -ContentType "application/json" -UseBasicParsing
 
	$appurl = $(($data.content | ConvertFrom-JSON)._links.self.href)
 
	$appurl = "$($rootappurl.split("/")[0..2] -join "/")$(($data.content | ConvertFrom-JSON)._links.self.href)"
 
	$appid = $appurl.split("/")[-1]
 
	$operationID = (($data.content | ConvertFrom-JSON)._embedded.communication | GM -Type Noteproperty)[0].name
}catch{
	write-output "Unable to create application instance"
	exit 1
}
 
write-verbose "#Allow HTML messages to be sent"
 
try{
	$postparams = @{"supportedMessageFormats"="Plain","Html"} | ConvertTo-JSON
 
	$data = Invoke-WebRequest -Uri "$rootappurl/$appid/communication/makeMeAvailable" -Method POST -Body $postparams -Headers @{"Authorization"="Bearer $authcwt"} -ContentType "application/json" -UseBasicParsing
}catch{
	write-verbose "HTML Messaging Already Configured"
}
 
write-verbose "#Send messages"
 
$i = 0
 
foreach($recipient in $recipients){
 
	if($recipient -notmatch "^sip:\S+"){
		$recipient = "sip:$recipient"
	}
	$i++
	$msgresult = sendmessage $operationID $rootappurl $appid $authcwt $recipient $messagesubject $messagebody $messagefooter $i
	if($msgresult){
		write-verbose "Message sent to $recipient"
	}else{
		write-verbose "Message not sent to $recipient"
	}
	start-sleep 1
}
 
write-verbose "#Delete App Instance"
$deleteapp = Invoke-WebRequest -Uri "$rootappurl/$appid" -Method DELETE -Headers @{"Authorization"="Bearer $authcwt"} -UseBasicParsing


Let’s say you make a change to your locale settings by directly editing the registry, modifying the HKCU\Control Panel\International\sTimeFormat key. Problem is that Windows doesn’t pick up these changes until you log off and back on again or restart explorer.exe. Now if you make the changes via Control Panel you don’t have to do this, so why do you if you modify the registry?

Well, when you use the UI to make changes to the locale or any other policy or environment settings, Windows sends a WM_SETTINGCHANGE broadcast to all Windows notifying them of the change to settings so they can refresh their config and you can do it too!

if (-not ("win32.nativemethods" -as [type])) {
    add-type -Namespace Win32 -Name NativeMethods -MemberDefinition @"
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(
    IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,
    uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
"@
}
 
$HWND_BROADCAST = [intptr]0xffff;
$WM_SETTINGCHANGE = 0x1a;
$result = [uintptr]::zero
 
[win32.nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE,[uintptr]::Zero, "Environment", 2, 5000, [ref]$result);

From the docs in reference to the lParam parameter of SendMessageTimeout, set to “Environment” in the above example:

This string can be the name of a registry key or the name of a section in the Win.ini file. When the string is a registry name, it typically indicates only the leaf node in the registry, not the full path.

When the system sends this message as a result of a change in policy settings, this parameter points to the string “Policy”.

When the system sends this message as a result of a change in locale settings, this parameter points to the string “intl”.

To effect a change in the environment variables for the system or the user, broadcast this message with lParam set to the string “Environment”.



If you’ve configured the Server Authentication Certificate Template GPO option, which determines the certificate that the machine uses for Remote Desktop connections, and applied it to 2008 R2 or older servers then you may find that you’re getting a lot of duplicate certificates being issued. It’s a problem with an easy solution but it’s not an obvious one.

You see, if you read the documentation for the setting (something which is helpfully not included in the GPO explanation text in the GPMC) you’ll soon discover that:

Important
You must set the certificate template’s attributes Template display name and Template name to the same value.

Due to a disparity in the way the API checks to see if a certificate already exists on the machine for this purpose if the Template Name is not the same as the Template Display name it fails to identify that it already has a matching certificate and so requests a new one.

This problem is resolved in Server 2012 R2. It’s possibly resolved in Server 2012 as well but I don’t have a box to hand I can test with.

As far as I can tell the “Do not automatically reenroll if a duplicate certificate exists in Active Directory” option has no impact on this issue.



Scenario: During an AD migration I needed to remove all of the certificates from a migrated user’s local store which had been issued by the old domain’s CA. Not simply for housekeeping reasons but because the new domain makes use of credential roaming and we didn’t want a load of old certificates taking up space in AD for no reason.

The following code will remove all certificates issued by from the Personal (My) store of the currently logged in user. If you wanted to narrow the criteria you can also filter on any of: Subject, Issuer, Thumbprint, FriendlyName, NotBefore, NotAfter or Extensions. You can also target different containers and switch between User (CurrentUser) and Machine (LocalMachine) certificate stores. As far as I’m aware there’s no way to do this for a user that isn’t currently logged in.

$Store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","Currentuser")
$store.Open("MaxAllowed")
$certs = $store.certificates | ?{$_.Issuer -eq "CN=My Issuing CA 1, DC=my, DC=domain"}
$certs | %{$store.remove($_)}
$Store.close()

See also https://www.angryadmin.co.uk/?p=600



If you’re running Server 2012 R2 in a vSphere 5.5 environment with version 10 hardware then you may have been greeted by this sight after a reboot (typically after installing Windows Updates):
Server 2012 R2 Boot Screen

Well you’re not alone, it’s a known issue with v10 hardware and multi-vCPU Windows 8/8.1 and Server 2012/2012 R2 VMs as documented here: http://kb.vmware.com/kb/2092807 – in essence if your VMs have been more than 2 or 3 months without a power cycle (either full Power Off or hard Reset) then there’s a very good chance it’ll hang on start-up after a soft reset.

The fix is fairly straightforward but you have to apply it to every affected machine as there doesn’t seem to be a way to set it globally – hopefully this will be fixed in vSphere 6 when it arrives.



If you’re using certificate-based authentication for your wired or wireless network and have the Lync 2013 client installed then you may find that your users start getting prompted to select a certificate when connecting to the network for the first time. This is because the Lync client issues users certificates with blank Subject fields and Windows can’t work out which certificate to use.

There’s a hotfix available from Microsoft here: http://support.microsoft.com/kb/2710995/en-us but unfortunately it’s not available via WSUS so you’ll have to push it to your clients “manually”. Personally I would have thought that certificate-based wireless authentication in environments running Lync were relatively common, enough to justify a proper patch, but apparently not.