Delete text-based Exchange log files, including those from IIS

I start with a warning – don’t ever manually remove Exchange Server database log files. Just don’t do it.

Exchange Server generates log files. Lots of log files. SOME of those aren’t useful and we just want to delete them. Others of those are important and should just be left alone! In general, Exchange Server log files break down into two different types: database log files and everything else.

It is unfortunate, but all Exchange Server log files have the same file extension: “.log”. Therefore, it isn’t possible to make decisions about log files based on the file extension. Only the directory/folder in which a file is located can be used for decision making purposes. This also applies to IIS (Internet Information Services) log files – they all end with “.log”.

On Exchange Server 2013 and Exchange Server 2016, there are three important directories:

‘C:\Program Files\Microsoft\Exchange Server\V15\TransportRoles\Logs’
‘C:\Program Files\Microsoft\Exchange Server\V15\Logging’
‘C:\inetpub\logs\LogFiles’

The first folder contains log files for “Client Access Services” (CAS). Even in Exchange Server 2016, the CAS log files are still maintained separately.

The second folder contains log files for “all other Exchange roles (but not CAS)”.

The third folder contains log files generated by IIS.

For all three folders, the locations shown are the default locations. Discovering non-default locations takes a lot of lines of PowerShell!

Generally speaking, Exchange Server will purge its own log file folders (the first two in the list above), after they reach a certain age (for example, 30 days) or a certain size (for example, 1.5 GiB).

However, IIS never automatically purges its log file folders. Never. Ever.

Especially in the case of IIS, log files can expand to fill all of the C: volume. In the Exchange Server case, Exchange usually reaches a “steady state” – a point at which log file removal balances out log file creation. However, when adding lots of new users or in the case of a new mobile OS version (this has been seen several times with iOS), a significant spike can be experienced in Exchange Server log files (especially the CAS log files).

This script will remove log files from the three named folders. By default, the log files must be at least 3 days old.

Other scripts may allow you to compress and store these log files. That’s great. And in certain situations, log files can provide a lot of information.

But for 99% of my clients – they just want to free up disk space – which this script allows.

The script is available here.

If you have questions or problems, send those to the author, michael at TheEssentialExchange dot com.


Follow me on twitter: @EssentialExch

Get-FrameworkVersion 1.6

In early December of 2017 I posted Get-FrameworkVersion 1.5. The cmdlet’s job was to examine a particular computer for all versions of the Windows .NET Framework installed on that computer. It has the capability of scanning the local computer or a remote computer. Remote computers can be accessed using CIM (with or without DCOM) or Windows RPC (also known as the Remote Registry Service).

This new version adds checking and reporting for BlockNetFramework registry key values. Each version of the .NET Framework can be blocked by creating a registry key value named BlockNetFramework<version> and setting the value data to a non-zero number. For example, BlockNetFramework472 with a value data of DWORD 1 would suppress the installation of .NET Framework 4.7.2. See the image below:

regedit image

Get the script on the TechNet Gallery: Display a list of all .NET Framework Versions installed on a computer.


Follow me on twitter! @EssentialExch

Get-FrameworkVersion

Get-FrameworkVersion.ps1 displays a list of all .NET Framework versions installed on a computer. While other scripts perform similar functionality, those I found were not well-behaved when they found an unknown version. Thus, the genesis for this version. Operation is simple:

PS C:\scripts> .\Get-FrameworkVersion.ps1
v2.0.50727       2.0.50727.4927   SP2
v3.0             3.0.30729.4926   SP2
v3.5             3.5.30729.4926   SP1
v4
		Client           4.7.02556
		Full             4.7.02556
v4.0
		Client           4.0.0.0

Current .NET Framework version: 4.7.1 on Windows 10 (4.7.02556 = release 461308)
PS C:\scripts>

I have posted the script on the TechNet Gallery: Display a list of all .NET Framework Versions installed on a computer.

The script is small enough that you can easily use it inside an Invoke-Command to execute on another computer. I will add native remote functionality in the next version.


Follow me on twitter! @EssentialExch

Clustersize, Blocksize, and Allocation Unit Size

Depending on where you see the terms used in Windows, you may see different names for the same thing. The clustersize of a volume, the blocksize of a volume, and the allocation unit size of the volume are all referring to the same value. Some tools report the blocksize as the physical blocksize, instead of the volume blocksize, which can lead to confusion between those terms. In this article, we will use the term clustersize.

Regardless of whether your disk volumes are for holding Exchange databases and logfiles, SQL databases and logfiles, Hyper-V VMs, or other large files; there is something you should do in order to properly optimize your access to those files. Adjust your clustersize. While we focus on Exchange here, most of the same ideas and concepts apply to many other applications.

The clustersize is used for several different things. What we care about:

  • The Master File Table (MFT) allocates storage on a disk volume based on the clustersize (that is, when a program requests to extend a file, the file is extended in multiples of the clustersize).
  • Exchange reads and writes to a disk volume based on multiples of the clustersize. Exchange also attempts to keep clusters contiguous.
  • Exchange allocates storage for logfiles and databases based on multiples of the clustersize. For example, a logfile is 1 MB in size (1 048 576 bytes). Exchange will allocate the 1 MB for a logfile all at once. If your clustersize is 4 096 (4 KB, the default) then the file consumes 256 clusters. If your clustersize is 65 536 (64 KB), then the file consumes 16 clusters. It is far more likely that 16 clusters can be allocated contiguously than 256 clusters.
  • Disk fragmentation is based on the contiguous or discontiguous placement of clusters.

The Exchange Server preferred architecture (PA) has long stated that logfiles and databases should reside on disk volumes where the clustersize is 64 KB. This is also true for the SQL Server PA. In earlier releases of Windows Server, it was also necessary to ensure that the initial location on a physical disk occur at a multiple of 64 KB. Beginning with Windows Server 2008 (??, I think), the OS automatically begins the initial allocation at the 1 MB boundary (which is an even multiple of 64 KB, as shown above).

In order to create a 64 KB volume, you can use whatever tool you prefer: format.com, diskpart.exe, WMI/CIM, or PowerShell’s Format-Volume. Or, if you don’t need to script this, you can use the GUI.

However, determining the clustersize isn’t the easiest thing in the world. There is no obvious place to find this value in the GUI. There is no obvious place to find this in PowerShell Get-* cmdlets (Get-Volume and Get-Partition do not return this information). If you want to go old-school, you use fsutil.exe in this manner:

PS C:\> fsutil.exe fsinfo ntfsinfo D:
NTFS Volume Serial Number :       0x2edec3c5dec38395
NTFS Version   :                  3.1
LFS Version    :                  2.0
Number Sectors :                  0x0000000c473befff
Total Clusters :                  0x00000000188e77df
Free Clusters  :                  0x00000000188e69ce
Total Reserved :                  0x0000000000000000
Bytes Per Sector  :               512
Bytes Per Physical Sector :       512
Bytes Per Cluster :               65536
Bytes Per FileRecord Segment    : 1024
Clusters Per FileRecord Segment : 0
Mft Valid Data Length :           0x0000000000010000
Mft Start Lcn  :                  0x000000000000c000
Mft2 Start Lcn :                  0x0000000000000001
Mft Zone Start :                  0x000000000000c000
Mft Zone End   :                  0x000000000000cca0
Resource Manager Identifier :     5012D937-F3EB-11E4-80BF-549F35094798
PS C:\>

And we see our answer in “Bytes Per Cluster”. If this is NOT the value we see, then we need to re-format the drive.

However, there is certainly a way to obtain this using PowerShell (or VBScript, for that matter). We switch to Windows Management Instrumentation (WMI). You perform the query this way:

PS C:\> Get-WmiObject -Class Win32_Volume  | ft -auto Label, Blocksize, Name

Label           Blocksize Name
-----           --------- ----
System Reserved      4096 \\?\Volume{998bc8e0-edd0-11e4-80b5-806e6f6e6963}
Data                65536 D:
                     4096 C:
PS C:\>

Again, if our data volume does not have a value of 64 KB, then we will need to reformat it.

Why the big deal about a larger clustersize? Well, it’s about efficiency (reducing fragmentation and optimizing I/O) and – perhaps surprisingly – that with low values of clustersize, it is possible to have a disk with lots of empty space – but files can no longer grow on the disk volume. This is because of an “artifact” of the MFT called the FAL (the File Attribute List). The FAL has a fixed maximum size of 256 KB. Among other things, the FAL keeps track of how many clusters are assigned to a given file. With the limit of 256 KB, the FAL can “only” store about one-and-a-half million fragmented clusters. On a volume with 4 KB clusters, the FAL will run out of space around at well less than 1 TB (depending on the overall fragmentation level of the volume). I have seen this occur with a customer on a database volume with a database size of less than 400 GB.

There used to be a KB article discussing this issue, but I can no longer find it online. Perhaps your favorite search engine can help you locate it. 🙂 KB 967351.

Exchange 2016 supports ReFS, which has a fixed clustersize of 64 KB (nothing smaller, nothing larger). According to recent postings about Exchange 2016, the Exchange 2016 Preferred Architecture will use ReFS instead of NTFS.

Please follow me on Twitter: @essentialexch

Postscript:
You’ll notice in the PowerShell example above that C:\ does not have a Label associated with it. In Windows Server 2012 R2, this is easily fixed in PowerShell:

PS C:\> Set-Volume -DriveLetter C -NewFileSystemLabel System
PS C:\> Get-WmiObject -Class Win32_Volume  | ft -auto Label, Blocksize, Name

Label           Blocksize Name
-----           --------- ----
System Reserved      4096 \\?\Volume{998bc8e0-edd0-11e4-80b5-806e6f6e6963}
Data                65536 D:
System               4096 C:
PS C:\> 

 

Finding Services Using non-System Accounts With PowerShell, v3

In 2008,I authored a PowerShell blogpost/script Finding Services Using non-System Accounts With PowerShell. It was a rewrite of an earlier VBscript blogpost/script from 2006, Finding Services Using non-System Accounts. That was a very basic rewrite and a very basic PowerShell script.

With this blogpost, I update the script to process the System Accounts present in Windows Server 182008 R2 and later (which is a super-set of those present in Windows Server 2003), as well as take full advantage of PowerShell’s capabilities, including detailed in-script help, using CIM, providing output as objects instead of text, and using Verbose to control detailed non-object output.

Unlike prior versions of this script, you may choose to provide a list of computers for processing via a file (the traditional ListOfComputers.txt), via a comma-separated command-line parameter, or specify an organizational unit within Active Directory which will be scanned for computer objects. Examples are provided within the in-script help.

Without further ado:

 

# Get-ServicesWithAccounts

#region help

<#
.SYNOPSIS
	For a given computer or list of computers, output the list of services which are running under specifically assigned accounts, which may be either a local account or a domain account.
.DESCRIPTION
	For a given computer or list of computers, examine the services installed on that computer and output a list of the services which are:

		[1] Not running as a privileged non-account local process
		[2] Not running as a service authority (NT Service\*)
		[3] Not running as the default ASP.Net user

	In short, the list of services which are running under specifically assigned accounts, which may be either a local account or a domain account.

	To use this script to evaluate remote computers, the remote computers must have Remote Management enabled and available through the remote computer's firewall.

	CIM cmdlet support was introduced as part of Windows Management Framework 3.0 (WMF 3.0), which included PowerShell 3.0. For computers running earlier versions of WMF/PowerShell, you must use WMI instead of CIM.

	There are three different ways to specify lists of computers (FileName, Computers, OrganizationalUnit). You can use all three at once,  if you wish.
.PARAMETER FileName
	This string parameter accepts a filename. The filename is opened and the contents are read. Each line is assumed to continue a single computer name.

	Before a line is evaluated, white-space characters which precede any text, and those which suffix any text are removed. White-space characters are determined by the System.Char.IsWhiteSpace() method.

	Empty lines are accepted, but ignored.

	Lines which begin with '#' are ignored.

	If filename cannot be opened, then a warning is issued to the script's warning stream (using Write-Warn).

	The FileName parameter defaults to 'not set'.
.PARAMETER Computers
	This string array parameter accepts a comma-separated list of computer names. Duplicate names are not removed. If you specify a computer name twice, it will be evaluated twice.

	Both WINS names (short names) the FQDNs (fully qualified domain names) are supported.

	Computer names which begin with '#' are ignored.

	The Computers parameter defaults to 'not set'.
.PARAMETER OrganizationalUnit
	This parameter has an alias of 'OU'.

	This string parameter accepts the name of an organizational unit, in distinguished name format.

	Active Directory is searched within this organizational unit, and all computer objects are returned. These computer objects are then evaluated.

	The SubTree parameter is used to indicate whether the search is only for this level of Active Directory, or if the search should include all subtrees of the current level (as well as the current level).

	The OrganizationalUnit parameter defaults to 'not set'.
.PARAMETER SubTree
	This boolean parameter accepts $true or $false. If the OrganizationalUnit parameter has not been specified, this parameter is ignored.

	If the SubTree parameter is not set, or if is set to $false, then subtrees of the current OrganizationalUnit are not searched for computer objects. Only the current level is searched.

	If the SubTree paramter is set to $true, then all available subtrees of the current OrganizationalUnit are also searched for computer objects.

	The SubTree parameter defaults to $false.
.PARAMETER NoErrors
	This switch parameter should either be set, or not set. The parameter defaults to 'not set'.

	If the NoErrors switch is set, and a computer cannot be contacted using either CIM or WMI (as appropriate), then a PSObject will be output for that computer indicating that the computer cannot be contacted.

	If the NoErrors switch is not set, then an error will be written to the script's error stream (using Write-Error).
.PARAMETER UseWMI
	This switch parameter should either be set, or not set. The parameter defaults to 'not set'. However, this script will detect whether it is being executed on PowerShell v1 or PowerShell v2 - in that case, UseWMI will be set automatically.

	CIM cmdlet support was introduced as part of Windows Management Framework 3.0 (WMF 3.0), which included PowerShell 3.0. For computers running earlier versions of WMF/PowerShell, you must use WMI instead of CIM.

	This script cannot feasibly detect whether a remote computer is using WMF 3.0 or later. If your human knowledge is aware that pre-3.0 versions of WMF are being used - then set this parameter.

	Please be aware that this script was not tested against computers using PowerShell v1.0 (I don't have any in my test environment). However, it was tested against computers using PowerShell v2.0. I believe that (with UseWMI) it should work against computers using PowerShell v1.0.
.PARAMETER Verbose
	Extensive execution-time information, including summary statistics, is provided if you set Verbose. The parameter defaults to 'not set'.
.INPUTS
	None.  You cannot pipe objects into this script.
.OUTPUTS
	This script outputs a series of PSObjects which contain the following properties: Computer, Account, ShortName, and FullName.

	A PSObject will only be output for a computer under two conditions:

		[1] The computer cannot be contacted and the NoErrors switch has been set.
		[2] One or more services running under specifically assigned accounts have been found on the computer. There will be one PSObject for each service which meets that criteria.
.NOTES
	NAME: Get-ServicesWithAccounts
	AUTHOR: Michael B.Smith
	LASTEDIT: December 17, 2014
	EMAIL: michael at TheEssentialExchange dot com
	VERSION: 3.0

	No warranties, express or implied, are available. This script is offered "as is".

	I hope this script works for you. If it does not, please tell me. I will attempt to figure out what is going on. However - no promises.

	Replaces and expands check-services.ps1

	Setting the Verbose switch will generate lots of processing information in the cmdlet, including interesting summary data.

	Since this script will operate against destination computers running PowerShell v1.0 and PowerShell v2.0, using AsJob and/or WorkFlows is not feasible.

	The accounts excluded from consideration by this script are show below. If a service is executing under any other account, the service will be reported. Excluded accounts:

		'NT Authority\LocalService',
		'NT Authority\Local Service',
		'NT Authority\System',
		'NT Authority\Network Service', 
		'NT Authority\NetworkService',
		'LocalSystem',
		'.\ASPNET',
		'NT Service\*'
.LINK
	http://essential.exchange/blog/2014/12/17/finding-services-using-non-system-accounts-with-powershell-v3.aspx
.EXAMPLE
	C:\> Get-ServicesWithAccounts
	C:\>

	If none of the Computer, FileName, or OrganizationalUnit parameters are specified, then this cmdlet does nothing.
.EXAMPLE
	C:\> Get-ServicesWithAccounts -FileName FileDoesntExist.txt
	WARNING: Could not read file
	C:\>

	If the specified filename cannot be opened and read, then this cmdlet outputs a warning.
.EXAMPLE
	C:\> Get-ServicesWithAccounts -Computers ., localhost, georgie, Win8-L7.example.local
	checkServicesOnComputer : The WinRM client cannot process the request. If the authentication scheme is different from
	Kerberos, or if the client computer is not joined to a domain, then HTTPS transport must be used or the destination
	machine must be added to the TrustedHosts configuration setting. Use winrm.cmd to configure TrustedHosts. Note that
	computers in the TrustedHosts list might not be authenticated. You can get more information about that by running the
	following command: winrm help config.
	At C:\Scripts\Get-ServicesWithAccounts.ps1:338 char:2
	+     checkServicesOnComputer $computer
	+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
	    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,checkServicesOnComputer
	C:\>

	In this case, all of ".", "localhost", and "Win8-L7" refer to the same computer - which has no special services. "Georgie" is a computer that does not exist.

	The UseWMI parameter was not specified, so the script checked the current OS. The current OS is Windows 8.1, so CIM was used instead of WMI.
.EXAMPLE
	C:\> Get-ServicesWithAccounts -Computers ., localhost, georgie, Win8-L7.example.local -UseWMI
	checkServicesOnComputer : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
	At C:\Scripts\Get-ServicesWithAccounts.ps1:338 char:2
	+     checkServicesOnComputer $computer
	+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
	    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,checkServicesOnComputer
	C:\>

	To compare against the non-UseWMI case, accessing Georgie still fails when using WMI, but the error is different.
.EXAMPLE
	C:\> Get-ServicesWithAccounts -Computers ., localhost, georgie, Win8-L7.example.local -NoErrors
	Computer                      Account                       ShortName                     FullName
	--------                      -------                       ---------                     --------
	georgie                       
	C:\>

	In this case, all of ".", "localhost", and "Win8-L7" refer to the same computer - which has no special services. "Georgie" is a computer that does not exist. With the NoErrors switch, the output is much easier to comprehend.

	If UseWMI had been set, the output would have been the same.
#>

#endregion

[CmdletBinding( SupportsShouldProcess = $false, ConfirmImpact = 'None' )]

Param (
	[Parameter( Mandatory = $false )]
	[string] $FileName = $null,

	[Parameter( Mandatory = $false )]
	[string[]] $Computers = $null,

	[Parameter( Mandatory = $false )]
	[Alias( 'OU' )]
	[string] $OrganizationalUnit = $null,

	[Parameter( Mandatory = $false )]
	[bool] $SubTree = $false,

	[Parameter( Mandatory = $false )]
	[switch] $NoErrors,

	[Parameter( Mandatory = $false )]
	[switch] $UseWMI
)

Set-StrictMode -Version 2.0

$arrExclude = 
	'NT Authority\LocalService',
	'NT Authority\Local Service',
	'NT Authority\System',
	'NT Authority\Network Service', 
	'NT Authority\NetworkService',
	'LocalSystem',
	'.\ASPNET'

$iAdmin    = 0
$iTotal    = 0
$iCount    = 0
$iError    = 0
$iEmpty    = 0
$iComment  = 0
$iExcluded = 0

$psversion = 1
if( Test-Path Variable:PSVersionTable )
{
	$psversion = $PSVersionTable.PSVersion.Major
}
Write-Verbose "Using PSVersion = $psversion"

function searchOU( [string] $ou, [bool] $subtree )
{
	Write-Verbose "About to search '$ou'"

	$objDomain              = New-Object System.DirectoryServices.DirectoryEntry( 'LDAP://' + $ou )
	$objSearcher            = New-Object System.DirectoryServices.DirectorySearcher
	$objSearcher.SearchRoot = $objDomain
	$objSearcher.Filter     = "(objectCategory=Computer)"
	if( $subtree )
	{
		Write-Verbose 'SearchScope is SubTree'
		$objSearcher.SearchScope = [System.DirectoryServices.SearchScope]::SubTree
	}
	else
	{
		Write-Verbose 'SearchScope is OneLevel'
		$objSearcher.SearchScope = [System.DirectoryServices.SearchScope]::OneLevel
	}

	$results = $objSearcher.FindAll()
	Write-Verbose "OU search returned $($results.Count) results"

	[string[]]$array = $null

	foreach( $result in $results )
	{
		Write-Verbose "Result added '$($result.Properties.dnshostname.Item(0))'"
		$array += $result.Properties.dnshostname.Item( 0 )
	}

	return $array
}

function makeObject( [string] $computer, [string] $account, [string] $shortname, [string] $fullname )
{
	$obj = '' | Select Computer, Account, ShortName, FullName

	$obj.Computer  = $computer.ToLower()
	$obj.Account   = $account.ToLower()
	$obj.ShortName = $shortname
	$obj.FullName  = $fullname

	return $obj
}

function checkServicesOnComputer( [string] $strComputer )
{
	[bool] $bResult = $false
	[object] $results = $null

	Write-Verbose "Checking computer $strComputer" 

	$error.Clear()
	if( $useWMI -or ( $psversion -lt 3 ) )
	{
		$results = Get-WmiObject Win32_Service -ComputerName $strComputer -Property Name, StartName, Caption -EA 0 | 
			Select Name, StartName, Caption
		$bResult = $?
	}
	else
	{
		$results = Get-CimInstance Win32_Service -ComputerName $strComputer -Property Name, StartName, Caption -EA 0 | 
			Select Name, StartName, Caption
		$bResult = $?
	}

	if( -not $bResult )	# if( $bResult -eq $false )
	{
		$Script:iError++
		if( -not $noErrors )
		{
			Write-Error $error[ 0 ].ToString()

			### If we use 'throw' instead of write-error, then the error becomes terminating,
			### when the user does not use -noerrors. That is a non-optimal behavior.
			### throw $error[ 0 ]
		}
		else
		{
			makeObject $strComputer '' '' ''
			Write-Verbose "....An error has occurred, but was suppressed; no results were returned from computer $strComputer"
		}

		return
	}

	if( $null -eq $results )
	{
		Write-Verbose "No error occurred, but no results were returned on computer $strComputer"

		return
	}

	Write-Verbose "I have found $($results.Count) services on computer $strComputer"

	$iIncluded = 0
	foreach( $result in $results )
	{
		$account = $result.StartName
		$accName = $result.Name

		if( [String]::IsNullOrEmpty( $account ) )
		{
			Write-Verbose "No account was specified for service $accName"

			$Script:iExcluded++
			continue
		}

		if( $arrExclude -contains $account )
		{
			### this is too verbose
			### Write-Verbose "Account $account excluded for service $accName"

			$Script:iExcluded++
			continue
		}

		$acctLen   = $account.Length

		$ntService = "NT Service\"
		$ntServLen = $ntService.Length

		if( ( $acctLen -gt $ntServLen ) -and ( $account.SubString( 0, $ntServLen ) -eq $ntService ) )
		{
			Write-Verbose "NT Service account excluded: $account for $accName"

			$Script:iExcluded++
			continue
		}

		$iIncluded++

		### i should actually check the SID for '-500'. but i don't.

		$adminR = "\administrator";  ## admin-from-the-right
		if( ( $acctLen -ge $adminR.Length ) -and
		    ( $account.SubString( $acctLen - $adminR.Length ) -eq $adminR ) )
		{
			Write-Verbose "Account $account is an administrator account (1)"
			$Script:iAdmin++
		}

		$adminL = "administrator@";  ## admin-from-the-left
		if( ( $acctLen -ge $adminL.Length ) -and
		    ( $account.SubString( 0, $adminL.Length ) -eq $adminL ) )
		{
			Write-Verbose "Account $account is an administrator account (2)"
			$Script:iAdmin++
		}

		Write-Verbose "Account $account, computer $strComputer, service $accName"

		makeObject $strComputer $account $accName $result.Caption
	}

	$Script:iTotal += $iIncluded
}

function doProcessSingleComputer( [string] $computer )
{
	if( [String]::IsNullOrEmpty( $computer) )
	{
		Write-Verbose 'Computer name is null or empty (1)'

		$Script:iEmpty++
		return
	}

	$computer = $computer.Trim()
	if( [String]::IsNullOrEmpty( $computer ) )
	{
		Write-Verbose 'Computer name is null or empty (2)'

		$Script:iEmpty++
		return
	}

	if( '#' -eq $computer.SubString( 0, 1 ) )
	{
		Write-Verbose 'Computer name is a comment'

		$Script:iComment++
		return
	}

	Write-Verbose "About to process $computer"
	checkServicesOnComputer $computer
	$Script:iCount++
}

function doProcessArray( [string[]] $computerArray )
{
	if( ( $null -eq $computerArray ) -or ( $computerArray.Count -le 0 ) )
	{
		return
	}

	Write-Verbose "About to process computerArray containing $($computerArray.Count) items"

	foreach( $computer in $computerArray )
	{
		doProcessSingleComputer $computer
	}
}

function doProcessFile( [string] $filename )
{
	if( [String]::IsNullOrEmpty( $filename ) )
	{
		return
	}

	Write-Verbose "filename = $filename"
	$computers = Get-Content $filename -EA 0
	if( !$? -or ( $null -eq $computers ) -or ( $computers.Count -le 0 ) )
	{
		Write-Warning "Could not read file"
		Write-Verbose "Could not read file"
		return
	}
	Write-Verbose "$filename contains $($computers.Count) lines"

	doProcessArray $computers
}

function doProcessOU( [string] $ou, [bool] $subtree )
{
	if( [String]::IsNullOrEmpty( $ou ) )
	{
		return
	}

	$computerArray = searchOU $ou $subtree
	if( $computerArray -and ( $computerArray.Count -gt 0 ) )
	{
		doProcessArray $computerArray
	}
}

	###
	### Main
	###

	$start = Get-Date
	Write-Verbose "Script starts: $(Get-Date $start -Format u)"

	doProcessFile  $fileName
	doProcessArray $computers
	doProcessOU    $organizationalUnit $subtree

	Write-Verbose ""
	Write-Verbose "Processing complete."
	Write-Verbose "Total computers processed: . . $iCount"
	Write-Verbose "Total excluded services: . . . $iExcluded"
	Write-Verbose "Total Administrator services:  $iAdmin"
	Write-Verbose "Total special services:  . . . $iTotal"
	Write-Verbose "Total empty lines: . . . . . . $iEmpty"
	Write-Verbose "Total comment lines: . . . . . $iComment"
	Write-Verbose "Total errors:  . . . . . . . . $iError"

	$end = Get-Date
	Write-Verbose "Script end: $(Get-Date $end -Format u)"
	$diff = $end - $start
	Write-Verbose "Script elapsed: $($diff.ToString())"

 

 

Follow me on Twitter: @essentialexch

Testing Exchange Autodiscover with PowerShell

Exchange Autodiscover is deceptively simple. Until it doesn’t work. Then, repairing autodiscover can be surprisingly challenging.

Microsoft does provide the Microsoft Connectivity Analyzer which can (among other things) give you detailed information about your autodiscover situation. However, it works “from the outside to the inside”. This PowerShell allows you to work “from the inside to the outside”.

The script below allows you to test autodiscover to a specific server. If it succeeds, but your overall autodiscover fails, then you have a far reduced set of things to investigate, in order to repair your autodiscover deployment. This script will allow you to test internally or externally (given that your firewall/router has the appropriate NAT forwarding/translation rules and DNS is properly configured). If an internal test succeeds, but an external test fails – this points you to load-balancer/firewall/router. If the internal test fails – then you may have several items to check. Read the source of the script below for the most common issues.

I am developing a script that works “the same way” as Outlook is documented to work for autodiscover – with more insight to the various steps. While I have it working today, it’s not a pretty script. 🙂 I will post it when it is more clean.

Enjoy.


###
### Test-AutoD.ps1
###
### This script performs a basic test of Exchange autodiscover for a given
### hostname.
###
### This script does NOT behave exactly the same as Outlook autodiscover or
### ExRCA.com autodiscover. At this writing, that script is still in
### development.
###
### Instead, given a particular hostname, this script determines what the
### specified host returns as an autodiscover response. If you have chosen
### your real autodiscover host, the responses will be the same.
###
### This script allows you to test autodiscover to a specific host. None
### of the Microsoft provided tools give you that flexibility.
###
### In general, you must specify either useDefaultCredentials OR specify
### all three of username, password, and emailAddress.
###
### If you specify the "mobile" argument, then the request sent to
### autodiscover is for Exchange ActiveSync, versus the request that is
### sent for Outlook.
###
### The usage for useDefaultCredentials is explained within the source
### of the script. Look below.
###
### If this script works, but your Exchange autodiscover does not, there
### are probably five primary reasons:
###
###	[1] Misconfigured permissions on the Autodiscover virtual directory
###	[2] Misconfigured (or missing) Service Connection Point (SCP)
###	[3] Misconfigured DNS for $hostname
###	[4] Misconfigured SRV record
###	[5] Misconfigured permissions on the reverse proxy
###
### This is not a list of all possible issues.
###
### Michael B. Smith
### michael at TheEssentialExchange dot com
### October, 2014
###
### No warranties, express or implied, are available. This script is offered
### "as is".
###
### I hope this script works for you. If it does not, please tell me. I
### will attempt to figure out what is going on. However - no promises.
###

Param(
    [string]$username     = $null,
    [string]$password     = $null,
    [string]$emailAddress = $null,
    [string]$hostname     = 'autodiscover.example.com',
    [switch]$mobile,
    [switch]$useDefaultCredentials
)

if( [String]::IsNullOrEmpty( $hostname ) )
{
	throw "You must provide a hostname"
	return
}

$uri = 'https://' + $hostname + '/Autodiscover/Autodiscover.xml'
'Autodiscover URI:'
' '
$uri
' '

if( -not ( $useDefaultCredentials -ne $null -and $useDefaultCredentials.IsPresent ) )
{
	if( [String]::IsNullOrEmpty( $username ) )
	{
		$str = Read-Host "Enter username for request"
		if( [String]::IsNullOrEmpty( $str ) )
		{
			throw "You must provide a username"
			return
		}

		$username = $str
	}

	if( [String]::IsNullOrEmpty( $password ) )
	{
		$str = Read-Host -AsSecureString "Enter password for $username"
		if( [String]::IsNullOrEmpty( $str ) )
		{
			throw "You must provide a password"
			return
		}

		$cred = New-Object System.Management.Automation.PSCredential( $username, $str )
		$password = $cred.GetNetworkCredential().Password

		$cred = $null
		$str  = $null
	}

	if( [String]::IsNullOrEmpty( $emailAddress ) )
	{
		$str = Read-Host "Enter email address for $username"
		if( [String]::IsNullOrEmpty( $str ) )
		{
			throw "You must provide an emailaddress"
			return
		}

		$emailAddress = $str
	}
}

Set-StrictMode -version 2.0 

if( $mobile )
{
	### a mobile autodiscover request returns far less data than an Outlook autodiscover request
	### a mobile device doesn't need much information to configure ActiveSync

	$autoDiscoverRequest = @"
				$emailAddress
				http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006
"@
}
else
{
	$autoDiscoverRequest = @"
				$emailAddress
				http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a
"@
}

$autoDiscoverRequest = $autoDiscoverRequest.Replace( "`t", '' )  ### remove tab characters from the autodiscoverrequest. Exchange doesn't like those tabs.

'This is the autodiscover request that will be submitted:'
' '
$autoDiscoverRequest
' '

$req = New-Object System.Net.WebClient
$req.Encoding = [System.Text.Encoding]::UTF8

if( $useDefaultCredentials )
{
	### this means use Kerberos or NTLM authentication. it will only work if the autodiscover
	### client is joined to the domain and the computer can build a connection to a DC (so,
	### the computer is on a LAN or connected via a VPN or connected via DirectAccess).

	$req.UseDefaultCredentials = $true
}
else
{
	### Basic authentication is VERY basic. The username and password are turned into base64 and
	### separated by a colon. That's all. Never use except with SSL!

	$auth = 'Basic ' + [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes( $username + ':' + $password ) )
	$req.Headers.Add( 'Authorization', $auth )
}

### autodiscover has knowledge about user-agents. i found that surprising.
### the user-agent below is for Office 2013 Pro with SP1

$req.Headers.Add( 'Content-Type', 'text/xml' )
$req.Headers.Add( 'User-Agent',   'Microsoft Office/15.0 (Windows NT 6.1; Microsoft Outlook 15.0.4641; Pro)' )

$webpage = $null

try
{
	$webpage = $req.UploadString( $uri, $autoDiscoverRequest )
}
catch
{
	throw $_
	return
}

'This is the autodiscover response:'
' '

if( $webpage -eq $null )
{
	write-error "Webpage response is empty."
	return
}

if( $webpage -is [System.String] )
{
	$webpage
}
else
{
	$webpage.InnerXml
}

$webpage = $null
$req     = $null

' '
'Done'

Follow me on twitter @essentialexch

 

PowerShell Quick Script: Would You Like to Play a Game?

I teach a three-day course for System Administrators. I call it "Pragmatic PowerShell" – and that's exactly what it is about. Teaching administrators what they need to know about using PowerShell.

Of course, discoverability is a large part of the process. So we cover the standard stuff: Active Directory, DNS, DHCP, Exchange, and Lync. But what I find is that while most SysAdmins are comfortable with executing "one command, one result", most of them don't understand the concepts behind looping, functions, and conditional statements. And that's fine – if they weren't mainframe SysAdmins, they probably never had to write scripts before.

Note: Mainframe SysAdmins have been writing scripts for many decades; Job Control Language (JCL) on IBM mainframes, Work Flow Language (WFL) on Burroughs mainframes, Symbolic Stream Generator (SSG) on Univac mainframes, etc. etc.

So, it is important to cover these things to the students in ways that they can understand, and hopefully, to have a little fun with it at the same time.

Therefore, I went back and found a few of the simple computer games I used to play. In the 1970's and 1980's, many BASIC-based computer games were available, and these were commonly published in books and magazines and on BBSs (Bulletin Board Systems). People would buy the books or the magazines and then type the games into their computer. YES, actually type them!

I was six years-old when Neil Armstrong landed on the moon. I remember watching the TV transmission, sitting at my grandmother's house. Like most boys of my age, I wanted to grow up and become an astronaut! Alas, it was not to be.

But playing computer games, I never lost that desire. And it took me, a decade later, to study Physics in college.

"Lunar Lander", or "Lunar Exploration Module", or "Rocket" were common versions of programs that simulated a landing on the moon. Usually with some ASCII graphics.

Here is a PowerShell version (with some minor enhancements) of "Rocket".

Would you like to play a game?

 


###
### Rocket.ps1
###
### Algorithm is from the original Jim Storer "Lunar Lander" Basic 
### program from 1969
###
### Adapted from
### 	http://www.vintage-basic.net/bcg/rocket.bas
###	Retrieved on 2014-09-15
###
###	with a couple of minor bugfixes.
###
### Original program source came from "Basic Computer Games",
### first published in 1978. ISBN-10: 0894800523.
###
### This was one of the very first computer games that I ever
### played, a version in the late 1970's. Until graphics became
### more common (early 1980's), text games were the prevalent
### versions of all games. I can remember playing this game on DEC
### PDP computers (loaded from paper tape) and from original Apple
### computers (loaded from cassette tape).
###
### Michael B. Smith
### michael at TheEssentialExchange dot com
### September, 2014
###

###
### Newtonian one-dimensional physics says:
###
###	x' = x + vt + (1/2) * a * t * t
###
### Deriving this equation is trivial and left as an
### exercise for the reader. (Hint: use induction.)
###
### This program implements that algorithm, adjusted for
### the gravitational attraction of the moon.
###
### On the Earth, the gross value of gravitational
### acceleration is 32 feet/second/second.  The value of
### the gravitational acceleration on the moon is about
### one-sixth of that amount ( 16.6% ). This program
### uses a value of 5 feet/second/second. (Which is 
### close - the calculated value is 5.31 ft/sec/sec,
### which is only a 6.2% difference).)
###
### When originally written, floating point math was
### much more expensive than integer math. So most of
### the calculations will provide integer results.
### Newer versions of this program (based on lunar.bas
### and on lem.bas) use floating point math and will
### provide slightly more accurate results.
###
### However - you may find that this game is more
### challenging than you expect. :)
###

function GetYorN( [string] $prompt )
{
	while( 1 )
	{
		$answer = Read-Host $prompt
		if( $answer -eq "n" -or $answer -eq "no" )
		{

			return "n"
		}
		if( $answer -eq "y" -or $answer -eq "yes" )
		{
			return "y"
		}
	}	
}

function print-header
{
	" " * 30 + "Rocket"
	" " * 15 + "Creative Computing  Morristown, New Jersey"
	" "
	" "
	" "
	"Apollo Lunar Landing Simulation"
	"------ ----- ------- ----------"
	" "
}

function print-instructions
{
	$a = GetYorN "Do You Want Instructions (Yes or No)?"
	if( $a -eq "n" )
	{
		return
	}

	"You are landing on the moon and have taken over manual"
	"control 1,000 feet above a good landing spot. You have"
	"a downward velocity of 50 feet/sec. 150 units of fuel"
	"remain."
	" "
	"Here are the rules that govern your Apollo space-craft:"
	" "
	"[1] After each second, the height, velocity, and remaining"
	"    fuel will be reported via Digby, your on-board computer."
	"[2] After the report, a '?' will appear. Enter the number"
	"    of units of fuel you wish to burn during the next"
	"    second. Each unit of fuel will slow your descent by"
	"    1 foot/sec."
	"[3] The maximum thrust of your engine is 30 feet/sec/sec"
	"    or 30 units of fuel per second. If you enter a value"
	"    which exceeds 30, your value will be set to 30. If"
	"    you enter a value less than zero, your value will be"
	"    set to zero."
	"[4] When you contact the lunar surface, your descent engine"
	"    will automatically shut down and you will be given a"
	"    report of your landing speed and remaining fuel."
	"[5] If you run out of fuel, the '?' will no longer appear"
	"    but your second-by-second report will continue until"
	"    you contact the lunar surface."
	" "
}

function print-introduction
{
	"Beginning landing procedure.........."
	" "
	"G o o d  L u c k ! ! !"
	" "
	" "
	"SEC  FEET      SPEED     FUEL     PLOT OF DISTANCE"
	" "
}

function process-contact
{
	"*** Moon Contact ***"       ## 670 580
	$h = $h + 0.5 * ( $v1 + $v ) ## 680
	if( $b -eq 5 )
	{
		## time delta = height / velocity
		$d = $h / $v         ## 720 690
	}
	else
	{
		## time delta = -v + ( Sqrt( v*v + ( h * ( 10 - 2 * b ) ) ) / ( 5 - b )
		$d = ( -$v + [Math]::Sqrt( ( $v * $v ) + $h * ( 10 - 2 * $b ) ) ) / ( 5 - $b ) ## 700
	}

	$v1 = $v + ( 5 - $b ) * $d   ## 730 710
	"Touchdown at " + ( $t + $d ).ToString( 'N1' ) + " seconds."  ## 760
	"Landing velocity " + $v1.ToString( 'N1' ) + " feet/second."  ## 770
	$f.ToString() + " units of fuel remaining."                   ## 780

	if( $v1 -eq 0 )
	{
		"Congratulations! A perfect landing!!"                      ## 800
		"Your license will be renewed........Later."                ## 805
	}

	if( [Math]::Abs( $v1 ) -ge 2 )
	{
		"***** Sorry, but you blew it!!!! *****"                    ## 820
		"Everyone on the Lunar Landing Module is dead."
		"Appropriate condolences will be sent to the next of kin."  ## 830
	}

	" "                                                                 ## 840 
	" "
	" "
}

function print-status
{
	$str = "{0,-4:N0} {1,-8:N0}  {2,-8:N0}  {3,-7:N0}  I" -f $t, $h, $v, $f
	$str += ( "*" * ( [int] ( $h / 15 / 2 ) ) ) + "*"
	$str
}

###
###	Main
###

	print-header
	print-instructions

	while( 1 )
	{
		print-introduction      ## 390   860

		$t = 0	   ## 455	## time in seconds, at beginning of simulation
		$h = 1000  ## 455	## height in feet, at beginning of simulation
		$v = 50    ## 455	## velocity in feet per second, at beginning of simulation
		$f = 150   ## 455	## available fuel in arbitrary units, at beginning of simulation

		[bool] $done = $false

		while( $f -gt 0 -or !$done )
		{
			if( $f -gt 0 )
			{
				print-status                 ## 490 610

				$b = [int] ( Read-Host '?' ) ## 500
				if( $b -lt 0 )  { $b = 0  }  ## 510
				if( $b -gt 30 ) { $b = 30 }  ## 520
				if( $b -gt $f ) { $b = $f }  ## 530
			}

			$v1 = $v - $b + 5               ## 540 660
			$f = $f - $b                    ## 560
			$h = $h - 0.5 * ( $v1 + $v )    ## 570

			if( $h -le 0 )                  ## 580
			{                               ## 580
				$done = $true           ## 580
				break                   ## 580
			}                               ## 580

			$t = $t + 1                     ## 590
			$v = $v1                        ## 600

			if( $f -gt 0 )                  ## 610
			{                               ## 610
				continue                ## 610
			}                               ## 610

			if( $b -ne 0 )                  ## 615
			{                               ## 620
				"*** Out of fuel. ***"  ## 620
			}                               ## 620

			print-status                    ## 640

			$b = 0	                        ## 650
		}

		process-contact                         ## 670 - 840

		$a = GetYorN "Another mission (Yes or No)?"
		if( $a -eq "n" -or $a -eq "no" )
		{
			" "
			"Apollo Launch Control, signing out."
			"------ ------ -------- ------- ----"
			" "

			break  ## exit while(1) loop
		}
	}

The line numbers are from rocket.bas available from the link shown in the script source.

Follow me on Twitter at @essentialexch

"WOULD YOU LIKE TO PLAY A GAME?" is a quote from "WarGames", starring a very young Matthew Broderick and Ally Sheedy, released in 1983.

Reporting on Client Access Server Configurations

Exchange 2010 and Exchange 2013 are rich with cmdlets providing you access to information regarding your Exchange environment.

However, as of Exchange 2013 CU6, there are 956 (!!) cmdlets. Knowing which cmdlets to use can be challenging. It can also be difficult to decide what information from a cmdlet's output is relevant.

Here is my attempt to consolidate output for Client Access Servers. This script is in daily use, and it works for me. I hope you find it useful.

If this script is executed on an Exchange Server, it will by default generate information for that server. You can change that by specifying the "-server <servername>" switch. The "-location <location-name>" switch is used to specify a physical location where a particular server resides. For example "London" or "Charlotte". The script defaults to using "location". FInally, if you specify "-Verbose", the script will output timing information. That is, how long the script took to execute and how long each cmdlet took to execute.

Enjoy!


##
## Dump-CasInformation.ps1
##
## Michael B. Smith
## September 8, 2014
## michael at TheEssentialExchange dot com
## http://Essential.Exchange/blog
##
## No warranties, express or implied. Use at your own risk.
##
[CmdletBinding(SupportsShouldProcess=$false, ConfirmImpact='None') ]

Param(
	[Parameter(Mandatory=$false)]
	[string] $location  = "location",

	[Parameter(Mandatory=$false)]
	[string] $server    = $env:ComputerName
)

Set-Strictmode -Version 2.0

$startScript = Get-Date
Write-Verbose "Dump-Casinformation script starts $($startScript | Get-Date -Format u)"

$location = $location.ToUpper()

if( ( Get-Command Get-ExchangeServer -EA 0 ) -eq $null )
{
	Write-Error "This script must be executed within an Exchange Management Shell"
	return
}

"****** GLOBAL  GLOBAL  GLOBAL ******" 

	$cmd = 'OutlookProvider'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-OutlookProvider

		$r | fl Name, CertPrincipalName, Server, TTL, 
			OutlookProviderFlags, 
			RequiredClientVersions, ExchangeVersion

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'ExchangeServer'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-ExchangeServer -Status

		$r | fl Name, Identity, Fqdn, Edition, 
			Site, OrganizationalUnit, ServerRole, 
			AdminDisplayVersion, ExchangeVersion, 
			Static*, Current*,
			Is*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

"****** $location  $location  $location ******"

	$cmd = 'ClientAccessServer'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-ClientAccessServer -Identity $server

		$r | fl Name, Identity, Fqdn,
			IsOutOfService,
			OutlookAnywhereEnabled,
			ClientAccessArray,
			ExchangeVersion,
			AlternateServiceAccountConfiguration,
			OutlookAny*, 
			AutoDiscover*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'OutlookAnywhere'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-OutlookAnywhere -server $server

		$r | fl Server, ServerName, Name, Identity, 
			MetabasePath, Path, 
			AdminDisplayVersion, ExchangeVersion,
			SSLOffloading,
			InternalHostname, InternalClientAuth*,
			InternalClientsRequireSSL,
			ExternalHostname, ExternalClientAuth*, 
			ExternalClientsRequireSSL,
			IISAuthenticationMethods,
			XropUrl,
			ExtendedPro*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'RpcClientAccess'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-RpcClientAccess -server $server

		$r | fl Name, Server, Identity,
			Responsibility,
			MaximumConnections,
			EncryptionRequired,
			BlockedClientVersions,
			ExchangeVersion

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'ActiveSync'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-ActiveSyncVirtualDirectory -server $server

		$r | fl Name, Server, VirtualDirectoryName, 
			WebsiteName, WebsiteSSLEnabled, CompressionEnabled,
			MetabasePath, Path,
			AdminDisplayVersion, ExchangeVersion,
			InternalUrl, InternalAuth*,
			ExternalUrl, ExternalAuth*,
			ActiveSyncServer,
			*AuthEnabled, ClientCertAuth,
			Mobile*,
			BadItemReportingEnabled, SendWatsonReport,
			Remote*,
			Extended*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'AutoDiscover'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-AutodiscoverVirtualDirectory -server $server

		if( -not [String]::IsNullOrEmpty( $r.InternalUrl ) )
		{
			Write-Warning "InternalUrl should be empty, instead is $($r.InternalUrl)"
		}
		if( -not [String]::IsNullOrEmpty( $r.ExternalUrl ) )
		{
			Write-Warning "ExternalUrl should be empty, instead is $($r.ExternalUrl)"
		}

		$r | fl Server, Name, Identity, 
			MetabasePath, Path, 
			AdminDisplayVersion, ExchangeVersion,
			InternalUrl, InternalAuth*, 
			ExternalUrl, ExternalAuth*, 
			*Authentication, ExtendedPro*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'ECP'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-EcpVirtualDirectory -server $server

		$r | fl Server, Name, Website, DisplayName, Identity, 
			MetabasePath, Path, 
			AdminDisplayVersion, ExchangeVersion,
			InternalUrl, InternalAuth*, 
			ExternalUrl, ExternalAuth*, 
			DefaultDomain, GzipLevel, *Enabled,
			*Authentication, ExtendedPro*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	if( ( Get-Command Get-MapiVirtualDirectory -EA 0 ) -ne $null )
	{
		## cmdlet not available in Exchange 2010

		$cmd = 'MAPI'
		"*** $cmd  $cmd  $cmd ***"
		$cmdStart = Get-Date
		Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

			$r = Get-MapiVirtualDirectory -server $server

			$r | fl Server, Name, Identity, 
				MetabasePath, Path, 
				AdminDisplayVersion, ExchangeVersion,
				InternalUrl, InternalAuth*, 
				ExternalUrl, ExternalAuth*, 
				IISAuth*,
				ExtendedPro*

		$cmdEnd = Get-Date
		$cmdDelta = $cmdEnd - $cmdStart
		Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
		Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"
	}
	elseif( $server -ne $env:ComputerName )
	{
		Write-Warning "Get-MapiVirtualDirectory is not available on this computer (so this computer is probably running Exchange 2010). If you are accessing a server running Exchange 2013 or higher, this information will be missing."
		" "
	}

#####

	$cmd = 'OAB'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-OabVirtualDirectory -server $server

		$r | fl Server, Name, Identity, RequireSSL,
			MetabasePath, Path, 
			AdminDisplayVersion, ExchangeVersion,
			InternalUrl, InternalAuth*, 
			ExternalUrl, ExternalAuth*, 
			PollInterval, OfflineAddressBooks,
			*Authentication, ExtendedPro*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'OWA'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

	## OWA is just too big to process effectively

		$r = Get-OwaVirtualDirectory -server $server

		$r | fl Server, ServerName, Name, Website, DisplayName, Identity, RequireSSL,
			MetabasePath, Path, 
			DefaultDomain, LogonFormat,
			AdminDisplayVersion, ExchangeVersion,
			InternalUrl, InternalAuth*, 
			ExternalUrl, ExternalAuth*, 
			VirtualDirectoryType, GzipLevel,
			Exchange2003Url, FailbackUrl, 
			LegacyRedirectType, RedirectToOptimalOWAServer,
			*Authentication, ExtendedPro*
		"...attribute list truncated, use 'Get-OwaVirtualDirectory -server $server | fl *' for full information"
		" "

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'PowerShell'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

	## this will return 2 entries for each server.
	"Note: the PowerShell Virtual Directory results include two vDirs for each server."

		$r = Get-PowerShellVirtualDirectory -server $server

		$r | fl Server, Name, Identity, RequireSSL,
			MetabasePath, Path, 
			OwaVersion, AdminDisplayVersion, ExchangeVersion,
			InternalUrl, InternalAuth*, 
			ExternalUrl, ExternalAuth*, 
			VirtualDirectoryType, 
			*Authentication, ExtendedPro*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

#####

	$cmd = 'WebServices'
	"*** $cmd  $cmd  $cmd ***"
	$cmdStart = Get-Date
	Write-Verbose "Dump-Casinformation command $cmd starts $($cmdStart | Get-Date -Format u)"

		$r = Get-WebServicesVirtualDirectory -server $server

		$r | fl Server, Name, Identity,
			MetabasePath, Path, 
			AdminDisplayVersion, ExchangeVersion,
			InternalUrl, InternalAuth*, 
			ExternalUrl, ExternalAuth*, 
			InternalNLBBypassUrl, GzipLevel, MRSProxyEnabled,
			*Authentication, ExtendedPro*

	$cmdEnd = Get-Date
	$cmdDelta = $cmdEnd - $cmdStart
	Write-Verbose "Dump-CasInformation command $cmd ends $($cmdEnd | Get-Date -Format u)"
	Write-Verbose "Dump-CasInformation command $cmd took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

"****** DONE  DONE  DONE ******"

$scriptEnd = Get-Date
$cmdDelta = $scriptEnd - $startScript
Write-Verbose "Dump-CasInformation script ends $($scriptEnd | Get-Date -Format u)"
Write-Verbose "Dump-CasInformation script took $($cmdDelta.TotalSeconds.ToString( 'N2' )) seconds"

Follow me on Twitter at @essentialexch

 

Reporting on primary and secondary SMTP addresses on Exchange objects

Most companies have a set of primary Internet domains (Exchange accepted domains) that they use to assign to users. However, a constant is that most companies also assign a secondary email address that has a domain which is identical for all of their users. That is true in my environment, and in that of all customers I have worked with in the past.

However, Exchange tends to generate (especially if you have migrated from legacy Exchange versions) far more email addresses than that for any given Exchange object (user, group, contact).

The script below reports on each user's primary SMTP address, plus a secondary SMTP address whose domain is specified as a parameter.

The script uses the Active Directory PowerShell module, which must be installed on the computer where this script is executed. The script does not use any Exchange specific features, thus the Exchange Management Shell is not required. I tested the script on Exchange 2010 and Exchange 2013, but it should work on Exchange 2007 as well.

A couple of techniques worth noting are used here. First, instead of a function, I use a filter. A filter is a very special kind of a function optimized for working with pipelines of objects. Inputs from the pipeline are passed to the filter using $_.

Second, I parse the proxyAddresses attribute. This attribute contains a list of all addresses that are assigned to a given user. It is an array (a collection) of all those addresses. Importantly, for each item in the collection, the address itself is prefixed by an address type. For SMTP addresses, the address type is "smtp". For FAX addresses, the address type is "fax". For X500 addresses, the address type is "x500". Etc. Also importantly, if the address type is capitalized, then a particular address is the primary address of that type – the default. If the address type is not capitalized, then the address is a secondary address. There may be any number of secondary addresses of a particular type. There may be only one primary address of a particular type.

In order to detect primary addresses, I use the "-ceq" operator in PowerShell. This is "cased equal". That means that the case of the letters is significant. By default, comparisons in PowerShell are not case significant.

Finally, Exchange objects will always have the proxyAddresses attribute populated. This fact is used to build LDAP query utilized to find Exchange objects.

I hope you find this useful!

##
## Get-PrimaryAndSecondary
##
## Michael B. Smith
## April, 2013
##
Param(
	[string]$secondaryDomain = "@TheEssentialExchange.com"
)

[int]$secondaryDomainLen = $secondaryDomain.Length

filter strip-Addresses
{
	$proxies = $_.proxyAddresses

	$primary   = ""
	$secondary = ""

	$object = "" | Select GivenName, Surname, sAMAccountName, PrimarySmtp, SecondarySmtp

	$object.GivenName      = $_.GivenName
	$object.SurName        = $_.SurName
	$object.sAMAccountName = $_.sAMAccountName

	foreach( $proxy in $proxies )
	{
		$len = $proxy.Length

		## note: "SMTP:".Length == 5

		## note: The primary SMTP address has a CAPITALIZED "SMTP:" prefix
		## all secondary SMTP addresses have a lowercase "smtp:" prefix

		## note: any interesting secondary proxy address will be longer than 
		## "SMTP:".Length + $secondaryDomainLen

		if( $len -gt 5 )
		{
			$prefix = $proxy.SubString( 0, 5 )
			$temp   = $proxy.SubString( 5 )	##strip off "smtp:", if present

			if( $prefix -ceq "SMTP:" )
			{
				$primary = $temp
				if( $secondary.Length -gt 0 )
				{
					break   ## we have both primary and secondary, 
						## we don't need to look any more
				}
			}
			elseif( $prefix -ceq "smtp:" -and $len -gt ( 5 + $secondaryDomainLen ) )
			{
				if( $temp.EndsWith( $secondaryDomain ) )
				{
					$secondary = $temp
					if( $primary.Length -gt 0 )
					{
						break   ## we have both primary and secondary, 
							## we don't need to look any more
					}
				}
			}
		}
	}

	$object.PrimarySmtp   = $primary
	$object.SecondarySmtp = $secondary

	$object
}

Import-Module ActiveDirectory

Get-AdUser -LDAPFilter "(&(objectCategory=user)(proxyAddresses=*))" `
	-Properties GivenName, SurName, proxyAddresses -ResultSetSize $null | 
	strip-Addresses

Follow me on twitter, @essentialexch

 

Determining the Exchange Version – without using Get-ExchangeServer – Update 2013

This is an update of my post Determining the Exchange Version – without using Get-ExchangeServer, from April 25, 2012. Since then, Exchange 2013 has been released! I've had several requests from people who are not PowerShell scripters to update that function. So here it is. I have repeated the text from the prior below.

If you write lots of Exchange scripts (as I do), and have several different versions of Exchange on which you need to run those scripts (as I do); you soon see the need for being able to determine what version of Exchange a particular server is running – and you may need to do this outside of the Exchange Management Shell.

This may be necessary because of different behaviors that are required with the Exchange Management Shell depending on the version of Exchange. It also may be required because the version of Exchange pre-dates the Exchange Management Shell (i.e., Exchange 2003). As an additional rationale, your script may need to load the Exchange Management Shell and cannot do that properly without knowing the version of Exchange that is being targeted (the process differs between Exchange 2007 and Exchange 2010 and Exchange 2013).

Thus, I've written a couple of functions that I wrap in a script to give me that information. Get-ExchangeServer, the function presented in this post, returns a simple object containing the name of the server examined, plus the version of Exchange that is running on that server. That is, in C pseudo-syntax:

struct result {
string Name;
string Version;
}

If the result of the function is $null, then the version could not be determined and the server targeted either does not exist or is (very likely) not running Exchange. If the server does not exist (or the firewall on the server prevents remote management via WMI) then an error is displayed.

The version information about Exchange is stored in the registry of each Exchange server. The script below shows some techniques for accessing the registry to obtain string (reg_sz) and 32-bit short integer (reg_dword) values while using PowerShell. Note that PowerShell has multiple mechanisms for accessing this information, including the so-called Registry Provider. I personally find using the WMI functions to be a bit easier to handle.

You can include these functions directly into your PowerShell profile, or you can dot-source the function on an as-needed basis.

Without further ado, enjoy!

###
### Get-ExchangeVersion
###
### Return the version of the specified Exchange server
###
### 2013-04-11
###	Updated to support Exchange Server 2013 and E16
###

Set-StrictMode -Version 2.0

$HKCR = 2147483648
$HKCU = 2147483649
$HKLM = 2147483650

function RegRead
{
	Param(
		[string]$computer,
		[int64] $hive,
		[string]$keyName,
		[string]$valueName,
		[ref]   $value,
		[string]$type = 'reg_sz'
	)

	[string]$fn = "RegRead:" ## function name

	try {
		$wmi = [wmiclass]"\\$computer\root\default:StdRegProv"
		if( $wmi -eq $null )
		{
			return 1
		}
	}
	catch {
		$error[0]
		write-error "$fn Could not open WMI access to $computer"
		return 1
	}

	switch ( $type )
	{
		'reg_sz'
			{
				$r = $wmi.GetStringValue( $hive, $keyName, $valueName )
				$value.Value = $r.sValue
			}
		'reg_dword'
			{
				$r = $wmi.GetDWORDValue( $hive, $keyName, $valueName )
				$value.Value = $r.uValue
			}
		default
			{
				write-error "$fn Unsupported type: $type"
			}
	}

	$wmi = $null

	return $r.ReturnValue
}

function Get-ExchangeVersion
{
	Param(
		[string]$computer = '.'
	)

	[string]$fn = "Get-ExchangeVersion:" ## function name

	if( $computer -eq '.' -or [String]::IsNullOrEmpty( $computer ) )
	{
		$computer = $env:ComputerName
	}

	## Exchange E16 (assumption!)
	## HKLM\Software\Microsoft\ExchangeServer\v16\Setup
	## MsiProductMajor (DWORD 16)

	## Exchange 2013
	## HKLM\Software\Microsoft\ExchangeServer\v15\Setup
	## MsiProductMajor (DWORD 15)

	## Exchange 2010
	## HKLM\Software\Microsoft\ExchangeServer\v14\Setup
	## MsiProductMajor (DWORD 14)

	## Exchange 2007
	## HKLM\SOFTWARE\Microsoft\Exchange\Setup
	## MsiProductMajor (DWORD 8)

	## Exchange 2003
	## HKLM\SOFTWARE\Microsoft\Exchange\Setup
	## Services Version (DWORD 65)

	$v = 0

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v16\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 16 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = 'E16'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v15\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 15 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2013'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v14\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 14 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2010'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\Exchange\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 8 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2007'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\Exchange\Setup' 'Services Version' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 65 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2003'
		return $obj
	}

	### almost certainly not an Exchange server

	return $null
}

Please follow me on Twitter, @essentialexch