After a fairly long period of absence I’ve started working on things that are actually worth sharing with people, but WordPress is not the place to do it because it really struggles with Markdown and syntax highlighting, especially if you’re self-hosting. So rather than trying to migrate all this crap to a new platform I’m just going to start afresh with Ghost over at

Windows 10 has made a lot of changes from previous versions, one of which is that you can no longer view System Properties as a non-admin user. This means you can no longer view/edit your user environment variables via System Properties.

There are 3 ways around this:

  • Use another method such as set or Powershell or direct registry editing
  • Go to Control Panel->User Accounts->Change My Environment Variables
  • Run "C:\WINDOWS\System32\rundll32.exe" sysdm.cpl,EditEnvironmentVariables to envoke the Environment Variables window directly

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 -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 "" -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 [email protected]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.


I’ve just completed my first ADFS 4.0 implementation and I’m quite astonished at just how many stupid, petty and easily resolved bugs it suffers from.

Case in point, if you change the service certificate it doesn’t change the https certificate bindings. This means that even though your ADFS server is using your new certificate for its communications, the web service is still using the old one. It’s not IIS any more so you have to manually recreate the bindings via netsh.

There also seems to be a bizzare issue that only affects IE whereby if your ADFS farm name DNS record is a CNAME rather than an A record, any authentication attempts will fail with a 400 BAD REQUEST error. This doesn’t affect other browsers because why would it, it’s a fucking mental thing to do.

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

<defaultProxy useDefaultCredentials="true" />

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 '[email protected]%'

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

	Sends IM via S4B
    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)
	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
    Author: Adam Beardwood
    Date: January 13, 2016
    v1.0: Initial Release
    v1.1: Bugfix for multiple recipients


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/"

		$postparams = @{"importance"="Normal";"sessionContext"="$(([guid]::NewGuid()).guid)";"subject"="$messagesubject";"telemetryId"=$null;"to"=$recipient;"operationId"=$operationID;"_links"[email protected]{"message"[email protected]{"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
		write-verbose "Unable to send message invite"
		return $false

	write-verbose "#Check state & get Conversation ID"


		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/events?ack=$ackid" -Method GET -Headers @{"Authorization"="Bearer $authcwt"} -ContentType "application/JSON" -UseBasicParsing

		$state = (($data.content | ConvertFrom-JSON)

		if($state.gettype().name -eq "string"){
			$state = $state[-1]


		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 = ($ | ?{$_.rel -eq "conversation"}).href.split("/")[-1]

	write-verbose "#Send Messages"
		$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
		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"

		$data = Invoke-WebRequest -Uri "$rootappurl/$appid/communication/conversations/$conversationID/messaging/terminate" -Method POST -Headers @{"Authorization"="Bearer $authcwt"} -UseBasicParsing
		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 = "Someone forgot to set a message so you're seeing this default instead"}
if(!$messageheader){$messageheader = "This is an automated alert from "}
if(!$messagefooter){$messagefooter = "This message was sent by a bot, there's no point in replying to it."}

write-verbose "#Get Autodiscover Information"

	$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
	write-output "Unable to get autodiscover information"
	exit 1
write-verbose "#Authenticate to server"

	$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
	write-output "Unable to authenticate, verify credentials and try again"
	exit 1
write-verbose "#Get application URLs"

	$data = Invoke-WebRequest -Uri "$oauthurl" -Method GET -Headers @{"Authorization"="Bearer $authcwt"} -UseBasicParsing

	$rootappurl = ($data.content | ConvertFrom-JSON)._links.applications.href
	write-output "Unable to get Application URLs"
	exit 1

write-verbose "#Create App Instance"

	$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
	write-output "Unable to create application instance"
	exit 1

write-verbose "#Allow HTML messages to be sent"

	$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
	write-verbose "HTML Messaging Already Configured"

write-verbose "#Send messages"

$i = 0

foreach($recipient in $recipients){
	if($recipient -notmatch "^sip:\S+"){
		$recipient = "sip:$recipient"
	$msgresult = sendmessage $operationID $rootappurl $appid $authcwt $recipient $messagesubject $messagebody $messagefooter $i
		write-verbose "Message sent to $recipient"
		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;
$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”.