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).



I spent a while recently wondering if it was possible to have certificates follow users between machines, in this case certificates used for 802.1x authentication, because I didn’t want our CA issuing a new certificate every time a user logged onto a new machine. It seemed logical that such a facility must exist but I couldn’t find anything useful until I stumbled upon it almost by accident while looking for something else certificate-related.

What I was after is Credential Roaming, which is basically a roaming profile system for certificates (and saved user credentials but that wasn’t really a consideration). Once enabled, credential roaming will store user credentials attached to their AD account object and download them to the local machine on logon, then on log off sync everything back up to the AD object again. Obviously there are things to consider here, especially if you have a lot of users and they have a lot of certificates, but you can set limits on the maximum store size (the default is 64k) and certificates are pretty small anyway – plus most of the features only work with Vista and later, but frankly if you’re still running XP then you’ve got to expect things not to work properly.



Quick and easy; Exchange creates an environment variable called “ExchangeInstallPath” which holds the install path for Exchange on a given server, this can be accessed via Powershell using $env:ExchangeInstallPath.

This can be useful if you need to call elements such as RemoteExchange.ps1 but aren’t sure if Exchange has been installed to the default location.



I saw this in someone’s signature on one of the Technet forums and though it was a really clever idea; nobody wants to put their email address in plaintext on the internet because it’ll just get hoovered up by spammers and most common obfuscation techniques are easily worked around by spambots, so how about…

[string](0..21|%{[char][int](23+("74778682874174878091987477868287237688239484").substring(($_*2),2))})-replace " "

Clever, isn’t it.

You can generate your own with the following code:

$out = $null
$email = "user@example.com"
foreach($char in $($email.tochararray())){$out += @([System.Convert]::ToUInt32($char)-23)}
$string = [string]$out -replace " "
"Your code is: [string](0..$($($out.length)-1)|%{[char][int](23+(`"$string`").substring((`$_*2),2))})-replace `" `""

Which should give an output that looks something like:

[string](0..15|%{[char][int](23+("94927891417897748689857823768886").substring(($_*2),2))})-replace " "

Email addresses with certain symbol characters may not encode properly.



If like me you maintain a number of IP address blacklists for various purposes (websites, firewalls, etc) then you probably find yourself thinking, from time to time, I wonder how many of these ranges overlap and could be merged down to clean these lists up a bit. I was hoping that there would be a nice, easy way to do this with Powershell but it doesn’t seem like there is.

Turns out there are some nice Perl libraries to help with this process in the form of Net::CIDR and its children. While my Perl is pretty dreadful, the following script will nonetheless take a collection of xxx.xxx.xxx.xxx-xxx.xxx.xxx.xxx style IP ranges, merge them down and output them to a file in the same format. You can also easily modify it to convert to/from CIDR-formatted address ranges, single addresses or any combination of the three; the full documentation for the Net::CIDR::Lite module is available on CPAN.

Obviously it requires Net::CIDR::Lite to be present on your system.

#!/usr/bin/perl -w
 
use Net::CIDR::Lite;
 
open(MYFILE, '>>data.txt');
 
my $cidr = Net::CIDR::Lite->new();
 
while (<DATA>) {
	chomp;
	$cidr->add_range($_);
}
 
for $r ( $cidr->list_range() ) {
	print MYFILE "$r\n";
}
 
close (MYFILE);
 
__DATA__
1.0.1.0 - 1.0.3.255
1.0.1.0 - 1.0.2.255
1.0.2.0 - 1.0.3.255


The following is the Powershell equivalent of unticking the box below for the folder “C:\temp”:

NTFS Permissions Inheritance

$acl = get-acl "C:\temp"
$acl.SetAccessRuleProtection($true,$true)
$acl | set-acl

Note that SetAccessRuleProtection takes two boolean arguments; the first turns inheritance on ($False) or off ($True) and the second determines whether the previously inherited permissions are retained ($True) or removed ($False).



Very simply returns True or False for a given list of hostnames or IP addresses depending on whether or not it can connect to TCP/3389 – a successful connection does not mean that you will be able to login, of course. If you’re running RDP on a non-standard port, you’ll need to adjust the script appropriately.

param(
     [parameter(Mandatory=$true,ValueFromPipeline=$true)][string[]]$computername
     )
 
$results = @()
 
foreach($name in $computername){
 
        $result = "" | select Name,RDP
        $result.name = $name
 
        try{
           $socket = New-Object Net.Sockets.TcpClient($name, 3389)
           if($socket -eq $null){
                 $result.RDP = $false
           }else{
                 $result.RDP = $true
                 $socket.close()
           }
        }
        catch{
                 $result.RDP = $false
        }
        $results += $result
}
 
return $results