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

Enumerating IP Addresses on Network Adapters using PowerShell

Regardless of whether a server is for Exchange, for SQL, or for any other use; a key component of the server’s configuration is the set of IP addresses assigned to it.

Many organizations apply all addresses (even server addresses) using DHCP. For servers, this is often based on reservations of the MAC addresses on those servers. In most cases, this works well. However, if the DHCP server(s) should be unavailable (for whatever reason), the server will not receive the desired address, and instead will receive an APIPA address (that is, an IPv4 address starting with “169.” as the first octet of the IPv4 address).

Regardless of the result, for many servers it is important to know if the IP addresses should change. The first step in that process is to be able to enumerate (list) all of the IP addresses for all of the network interfaces on the server.

There are a number of mechanisms available for obtaining this information from Windows; including but not limited to “ipconfig.exe”, WMI, “wmic.exe”, “netsh.exe”, and others. The challenge for using most of the available interfaces is that they provide a significant amount of extraneous information. Extraneous information makes the output very difficult to parse and use within scripts.

This blog post provides a script that enumerates all of the network interfaces available on a particular computer and the associated IP addresses. The second script, which you could schedule to run every 15-30 minutes, compares and contrasts any changes in IP addresses on a server and reports the change via a SMTP server (such as Exchange Server).

The first script:

 

###
### IP-Address-Check.ps1
###
### This script lists all Ethernet adapters supporting IP on a computer and
### the associated IP addresses - both IPv4 and IPv6. A simple object is
### output containing the name of the interface, an array containing the IPv4
### addresses, and an array containing the IPv6 addresses.
###
### While this information is available from netsh.exe, the format of the
### output from netsh.exe is not conducive for easy re-use or parsing.
###
### Michael B. Smith
### michael at smithcons dot com
### http://essential.exchange
### February 28, 2012
###

Set-StrictMode -Version 2.0

$HKLM = 2147483650
$wmi  = [wmiclass]"\root\default:StdRegProv"

function RegRead
{
	Param(
		[int64] $hive,
		[string]$keyName,
		[string]$valueName,
		[ref]   $value,
		[string]$type = "REG_SZ"
	)

	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
			}
		'reg_qword'
			{
				$r = $wmi.GetQWORDValue( $hive, $keyName, $valueName )
				$value.Value = $r.uValue
			}
		'reg_binary'
			{
				$r = $wmi.GetBinaryValue( $hive, $keyName, $valueName )
				$value.Value = $r.uValue
			}
		default
			{
				write-error "ERROR: RegRead $hive $keyName $valueName"
				return 1
			}
	}

	return $r.ReturnValue
}

function EnumerateAdapters
{
	$nicSettings = gwmi Win32_NetworkAdapterSetting -EA 0
	foreach( $nicSetting in $nicSettings )
	{
		$nicElement = [wmi] $nicSetting.Element
		if( $nicElement.AdapterType -eq "Ethernet 802.3" )
		{
			$nicConfig = [wmi] $nicSetting.Setting ## is of type Win32_NetworkAdapterConfiguration
			$nicGUID = $nicConfig.SettingID
			if( $nicConfig.IPEnabled -eq $true )
			{
				$hive = $HKLM
				$keyName = "System\CurrentControlSet\Control\Network\" +
					"{4D36E972-E325-11CE-BFC1-08002BE10318}\" + 
					$nicGUID + 
					"\Connection"
				$valueName = "Name"

				$name = ''
				$result = RegRead $hive $keyName $valueName ( [ref] $name ) 'reg_sz'
				if( $result -ne 0 )
				{
					$name = ""
				}

				$arrIPListPublic_v6 = @()
				$arrIPListPublic_v4 = @()

				foreach( $ip in $nicConfig.IPAddress )
				{
					if( $ip.IndexOf( ':' ) -ge 0 )
					{
						$arrIPListPublic_v6 += $ip
					}
					else
					{
						$arrIPListPublic_v4 += $ip
					}
				}

				$obj = "" | select Name, IPv4Addresses, IPv6Addresses
				$obj.Name = $name
				$obj.IPv4Addresses = $arrIPListPublic_v4
				$obj.IPv6Addresses = $arrIPListPublic_v6

				$obj  ### output the object to the pipeline

				$arrIPListPublic_v4 = $null
				$arrIPListPublic_v6 = $null
			}

			$nicConfig = $null
		}
		$nicElement = $null
	}
	$nicSettings = $null
}

###
### Main
###

EnumerateAdapters

 

The second script:

 

###
### IP-Check-Controller.ps1
###
### Check to see if any IP addresses on a computer have changed. This may 
### be relevant in many configurations based on DHCP.
###
### Michael B. Smith
### michael at smithcons dot com
### http://essential.exchange
### February 28, 2012
###

Param(
	[string]$to  = "ip-report@example.com",
	[string]$mx  = "mx.example.com",
	[string]$sub = "The IP address information has changed at $($env:ComputerName)",
	[string]$fr  = "ip-check-controller@example.local"
)

[string]$nl = "`r`n"

###
### Main
###

if( ( Test-Path "Addr-old.txt" -PathType Leaf ) )
{
	rm "Addr-old.txt"
}
if( ( Test-Path "Addr-new.txt" -PathType Leaf ) )
{
	mv "Addr-new.txt" "Addr-old.txt"
}

.\IP-Address-Check | out-file "Addr-new.txt"

$result = Compare-Object -ReferenceObject $(gc "Addr-old.txt") -DifferenceObject $(gc "Addr-new.txt") -PassThru
if( $result -eq $null )
{
	## no changes to file
	exit
}

### if we get here, the files are different

$text =
	"A change has occurred in the IP addresses or active adapters.$nl$nl"+
	"The changes found are:$nl"

foreach( $line in $result )
{
	$text += "`t" + $line + $nl
}

$text += $nl + "The new full configuration is:$nl"
	$lines = gc "Addr-New.txt"
	foreach( $line in $lines )
	{
		$text += "`t$line$nl"
	}

$text += $nl + "The old full configuration is:$nl"
	$lines = gc "Addr-Old.txt"
	foreach( $line in $lines )
	{
		$text += "`t$line$nl"
	}

Send-MailMessage -To $to -Subject $sub -From $fr -Body $text -SmtpServer $mx -Priority High

 

Typically, instead of scheduling the second PowerShell script, you would schedule a BAT script, as shown below:

 

@echo off
REM
REM IP-Check-Controller.bat
REM
REM Michael B. Smith
REM michael at smithcons dot com
REM http://essential.exchange
REM February 28, 2012
REM

powershell.exe -command ".\IP-Check-Controller.ps1"

 

Until next time…

If there are things you would like to see written about, please let me know.

 


Follow me on twitter: @EssentialExch

Sending an email to users whose password is about to expire, a PowerShell Rewrite

In September of 2005, I wrote a blog post named “Sending an e-mail to users whose password is about to expire“. Written in VBScript, it was one of my most popular blog posts of all time. I still have clients of mine that use it and I get occaisional email questions regarding it.

However, it is certainly showing its age!

There are other solutions available now, for free. However, the other solutions don’t meet all of my needs. (As always, I encourage you to choose the solution that best meets your needs.)

In my case, I need to be able to support:

  • Fast and efficient searching of Active Directory
  • Support for Fine Grained Password Policies (FGPPs, also known as Password Settings Objects or PSOs)
  • Authenticated SMTP
  • SSL/TLS SMTP
  • Report to Administrative User only (via SMTP)
  • Report to Administrative User only (via console)
  • Report to end-user (via SMTP)

The script in this blog post meets all of those needs. And, it is now written in PowerShell instead of VBScript.

I also chose to use the .Net Framework (System.DirectoryServices) for access to Active Directory (as well as some ADSI), instead of using the Active Directory PowerShell cmdlets. This makes it possible to execute the script on pretty-much any domain-joined computer, instead of one that requires RSAT-ADDS to be installed. It also avoids some weirdness around certain values returned by the AD cmdlets not matching older cmdlets or the AD itself.

This script is designed to work with PowerShell v2.0. The only PowerShell v2.0 feature is use of the Send-MailMessage cmdlet and using splatting to call Send-MailMessage. If you need this on PowerShell v1.0, you must just write a replacement for Send-MailMessage (use System.Net.Mail – it’s not a big deal).

The script will work on any domain functional level (DFL). The DFL is relevant to whether Fine-Grained Password Policies (FGPP, also known as Password Settings Objects – PSOs) are in use or not. FGPPs can be used when the domain level is at Windows 2008 or higher.

Coming in at 767 lines, this is the longest single PowerShell script I believe I’ve posted. But it’s well documented and hopefully self-descriptive. There are some fairly advanced capabilities demonstrated in this script, so you may find it worthwhile to study it a bit. If you have questions, let me know.

###
### Send-MailToUsersWithExpiringPasswords
###
### The top third of the script is data acquisition (and well documented).
### The bottom two-thirds is simple email-sending and report writing.
###
### This is PowerShell v1 compatible EXCEPT for using Send-MailMessage. You can
### easily replace that using System.Net.Mail if you wish.
###
### Parameter information:
###	daysForEmail   - how many days before a password expires should a user receive warning emails
###	adminEmail     - the administrator's email address
###	adminEmailOnly - do not send email to users, only report to the administrator
###	SMTPfrom       - the From address for the SMTP message(s)
###	SMTPserver     - the server to be used for sending the SMTP message(s)
###	SMTPuser       - if credentials are required, the user for authenticating to the SMTP server
###	SMTPpassword   - if credentials are required, the password for the SMTPuser
###	anr            - instead of searching all users, only search for users matching the specified ANR string
###	SMTPuseSSL     - use an SSL/TLS connection, not a clear-text SMTP connection
###	Quiet          - if NOT set, a copy of the admin report is dumped to the pipeline as text
###	DontSendEmail  - Email is never sent to either users or admin
###

Param(
	[int]$daysForEmail = 14,
	[string]$adminEmail = "michael@smithcons.com",
	[switch]$adminEmailOnly,
	[string]$SMTPfrom,
	[string]$SMTPserver,
	[string]$SMTPuser,
	[string]$SMTPpassword,
	[string]$anr,
	[switch]$SMTPuseSSL,
	[switch]$Quiet,
	[switch]$DontSendEmail
)

### Using Set-StrictMode helps protect against wonky errors that get caught by the
### compiler in compiled languages. Specifically (from the helpfile for the cmdlet):
### -- Prohibits references to uninitialized variables (including uninitialized 
###    variables in strings).
### -- Prohibits references to non-existent properties of an object.
### -- Prohibits function calls that use the syntax for calling methods.
### -- Prohibits a variable without a name (${}).
###
### However, using strict mode means that extra care has to be taken when using
### hashtables and property value collections. You see that in this script every
### time you see the Item() accessor method being used.

Set-StrictMode -Version 2.0

### For information about ANR, see "Ambiguous Name Resolution" in
### http://technet.microsoft.com/en-us/library/cc978014.aspx

$domainObject = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$domainName   = $domainObject.Name
$domainRoot   = "LDAP://" + $domainName
$domainADSI   = [ADSI]$domainRoot
$domainMode   = $domainADSI.'msDS-Behavior-Version' ## Windows2008Domain is 3.

### domainMode is a PITA. System.DirectoryServices.ActiveDirectory.Domain.DomainMode and
### Microsoft.ActiveDirectory.Management.ADDomainMode have different values for the same
### enums!
###
### The first type is returned by System.DirectoryServices, the second type by the
### Get-ADDomain PowerShell cmdlet.
###
### That is why I ignore both of those potential access methods and use ADSI to access the
### value directly from the domain object. For specific information about those values, see:
### http://msdn.microsoft.com/en-us/library/cc223742(v=prot.10).aspx

[System.Int64]$Script:MaxPasswordAge = 0

function GetMaximumPasswordAge
{
	###
	### GetMaximumPasswordAge
	###
	### Retrieve the maximum password age that is set on the domain object. This is
	### normally set by the "Default Domain Policy".
	###

	if( $Script:MaxPasswordAge )
	{
		### Cache the value so that it only has to be retrieved once, converted
		### to an int64 once, and converted to days once. Win-win-win.

		return $Script:MaxPasswordAge
	}

	### Dealing with ADSI unfortunately also means dealing with COM objects.
	### Using ConvertLargeIntegerToInt64 takes the COM object and converts 
	### it into a native .Net type.

	[System.Int64]$Script:MaxPasswordAge = $domainADSI.ConvertLargeIntegerToInt64( $domainADSI.maxPwdAge.Value )

	### Convert to days
	### 	there are 86,400 seconds per day (24 * 60 * 60)
	### 	there are 10,000,000 nanoseconds per second

	[System.Int64]$Script:MaxPasswordAge = ( -$Script:MaxPasswordAge / ( 86400 * 10000000 ) )

	return [System.Int64]$Script:MaxPasswordAge
}

function newSecurePassword( [string]$password )
{
	###
	### newSecurePassword
	###
	### Take the normal string password provided and turn it into a 
	### secure string that can be used to set credentials.
	###
	### In PowerShell v2.0, this can be done with ConvertTo-SecureString.
	### That cmdlet isn't available in v1.0 though.
	###

	$secure = New-Object System.Security.SecureString

	$password.ToCharArray() |% { $secure.AppendChar( $_ ) }

	return $secure
}

function newPSCredential( [string]$username, [string]$password )
{
	###
	### newPSCredential
	###
	### Create a new PSCredential object containing the provided
	### username and plain-text password.
	###

	$pass = newSecurePassword $password

	$cred = New-Object System.Management.Automation.PSCredential( $username, $pass )
	$pass = $null

	return $cred
}

###
### We need to find all those user's who:
###	Are normal users                       0x00000200 ADS_UF_NORMAL_ACCOUNT
###	Are not disabled                       0x00000002 ADS_UF_ACCOUNTDISABLE
###	Do not have "password never expires"   0x00010000 ADS_UF_DONT_EXPIRE_PASSWD
###	Do not have "no password required"     0x00000020 ADS_UF_PASSWD_NOTREQD
###
###	Once we have the users, determine whether the user has a PSO
###	by examining msDS-PSOApplied (if the domain mode of the executing domain
###	is at Windows2008Domain or higher).
###
###	If the user has a PSO, load the PSO (and store it to a hashtable)
###	and evaluate the user's password against the PSO.
###
###	If the user does not have a PSO, evalute the user's password
###	against the Default Domain Policy Maximum Password Age (which
###	is found by the function GetMaximumPasswordAge above).
###
### The most efficient way to find users is to do an LDAP search. But building the
### proper search isn't that easy. We need a number of filters:
###
###	objectCategory=Person
###	(userAccountControl & ADS_UF_NORMAL_ACCOUNT)     <> 0
###	(userAccountControl & ADS_UF_ACCOUNTDISABLE)     == 0
###	(userAccountControl & ADS_UF_DONT_EXPIRE_PASSWD) == 0
###	(userAccountControl & ADS_UF_PASSWD_NOTREQD)     == 0
###
### Since userAccountControl is a BIT-FLAG attribute (meaning that individuals bits
### of the value control these options) then we need to be able to do a bit-wise
### LDAP search. It's important to realize that with a bit-wise search, the result
### of a particular filter is either not-one (!1, which is false) or not-zero (!0,
### which is true).
###
### So the next step in building our query is to use the LDAP bit-wise AND filter:
###
###	1.2.840.113556.1.4.803
###
### This is used to do a bit-wise AND of the attribute on the left to the value
### on the right. For example:
###
###	attribute:1.2.840.113556.1.4.803:=1024
###
### This means a bit-wise AND is done of the value of the attribute to the value
### 1024 (which is 0x400 in hexadecimal). If the result of that bit-wise AND is
### zero, then the value of the filter is false. If the result is non-zero, then
### the value of the filter is true. Putting a "!" in front of a false result
### makes the result true. Putting a "!" in front of a true result, makes it false.
###
### A combination of these two techniques makes it possible to scan for zero and
### non-zero bits (that is, those which are set to one and those which are set to
### zero).
###
### LDAP also supports a bit-wise OR filter, using the special value of:
###
###	1.2.840.113556.1.4.804
###
### Given the presence of AND and OR filters, it is possible to build very complex
### combination filters.
###
### A combination filter is built up of individual filters combined with either a
### logical OR ("|") or a logical AND ("&") and surrounded by parentheses.
###
###	(&
###		(objectCategory=Person)
###		(userAccountControl:1.2.840.113556.1.4.803:=512)
###		(!userAccountControl:1.2.840.113556.1.4.803:=2)
###		(!userAccountControl:1.2.840.113556.1.4.803:=65536)
###		(!userAccountControl:1.2.840.113556.1.4.803:=32)
###	)
###
### So, in pseudo-C code this is:
###
###	if( ( objectCategory == Person ) AND
###	    ( ( userAccountControl & ADS_UF_NORMAL_ACCOUNT     ) != 0 ) AND
###	    ( ( userAccountControl & ADS_UF_ACCOUNTDISABLE     ) == 0 ) AND
###	    ( ( userAccountControl & ADS_UF_DONT_EXPIRE_PASSWD ) == 0 ) AND
###	    ( ( userAccountControl & ADS_UF_PASSWD_NOTREQD     ) == 0 ) )
###	{
###		### we've got a matching user!
###	}
###
$ldapFilter =   "(&"							      +
			"(objectCategory=Person)"                             +
			"(userAccountControl:1.2.840.113556.1.4.803:=512)"    +
			"(!userAccountControl:1.2.840.113556.1.4.803:=2)"     +
			"(!userAccountControl:1.2.840.113556.1.4.803:=65536)" +
			"(!userAccountControl:1.2.840.113556.1.4.803:=32)"

if( $anr )
{
	###
	### using an ANR subquery allows us to reduce the result set from the LDAP query
	###
	$ldapFilter += "(anr=$anr)"
}

$ldapFilter += ")"

###
### build the LDAP search
###

$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.PageSize    = 1000
$directorySearcher.SearchRoot  = $domainRoot
$directorySearcher.SearchScope = "subtree"
$directorySearcher.Filter      = $ldapFilter

###
### load the properties we want
###

$directorySearcher.PropertiesToLoad.Add( "displayName"        ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "mail"               ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "pwdLastSet"         ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "sAMAccountName"     ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "userAccountControl" ) | Out-Null

if( $domainMode -ge 3 )
{
	### this attribute is only valid on Windows2008Domain and above
	$directorySearcher.PropertiesToLoad.Add( "msDS-PSOApplied" ) | Out-Null
}

$users = $directorySearcher.FindAll()

###
### All the data is in $users (or will be paged into $users).
### Build the necessary reports and emails.
###

$crnl = "`r`n"
$script:adminReport = ""

function line
{
	foreach( $arg in $args )
	{
		$script:adminReport += $arg + $crnl
	}
}

$now = Get-Date
$maximumPasswordAge = GetMaximumPasswordAge

line ( "Admin Report - Send Mail to Users with Expiring Passwords - Run Date/Time: " + $now.ToString() )
line " "
line "Parameters:"
line "        Days warning for sending email: $daysForEmail"
line "        Administrator email: $adminEmail"

if( $DontSendEmail )
{
	line "        Email will not be sent to either the administrator email or to user's email"
}
elseif( $adminEmailOnly )
{
	line "        Only the administrator will be sent email"
}

if( ![System.String]::IsNullOrEmpty( $SMTPfrom ) )
{
	line "        SMTP From address: $SMTPfrom"
}

if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
{
	line "        SMTP server: $SMTPserver"
}

if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
{
	line "        SMTP user: $SMTPuser"
}

if( ![System.String]::IsNullOrEmpty( $SMTPpassword ) )
{
	line "        SMTP password: $SMTPpassword"
}

if( $SMTPuseSSL )
{
	line "        UseSSL with SMTP is set"
}

line "        Maximum Password Age in Default Domain Policy = $maximumPasswordAge"
line "        Domain Mode $($domainObject.DomainMode.ToString())"
line ( "        User count = " + $users.Count.ToString() )

if( [System.String]::IsNullOrEmpty( $SMTPserver ) -and [System.String]::IsNullOrEmpty( $PSEmailServer ) -and !$DontSendEmail )
{
	Write-Error "No email server was specified via the SMTPserver parameter or the `$PSEmailServer environment variable."
	exit
}

if( ![System.String]::IsNullOrEmpty( $SMTPuser ) -and [System.String]::IsNullOrEmpty( $SMTPpassword ) )
{
	Write-Error "No SMTPpassword was specified."
	exit
}

if( ![System.String]::IsNullOrEmpty( $SMTPpassword ) -and [System.String]::IsNullOrEmpty( $SMTPuser ) )
{
	Write-Error "No SMTPuser was specified."
	exit
}

if( [System.String]::IsNullOrEmpty( $SMTPfrom ) )
{
	$SMTPfrom = "Password Administrator "
}

###
### some of the bitflags attached to the userAccountControl attribute
###

$ADS_UF_NORMAL_ACCOUNT     = 0x00200
$ADS_UF_ACCOUNTDISABLE     = 0x00002
$ADS_UF_DONT_EXPIRE_PASSWD = 0x10000
$ADS_UF_PASSWD_NOTREQD     = 0x00020

$psoCache = @{}

foreach( $user in $users )
{
	###
	### we spend some time being pretty careful dealing with the properties. this
	### allows us to verify we get the attributes we want, and for those that were
	### not present, we can establish reasonable defaults.
	###

	line " "

	$propertyBag = $user.properties
	if( !$propertybag )
	{
		line "error! null propertybag!"
		continue
	}

	$mail = ""
	$disp = ""
	$sam  = ""
	$pls  = 0
	$pso  = 0
	$uac  = 0
	$pwdX = $null

	$dispObj = $propertyBag.Item( 'displayname' )
	if( $dispObj -and ( $dispObj.Count -gt 0 ) )
	{
		$disp = $dispObj.Item( 0 )
		if( !$disp ) { $disp = "" }
	}
	$dispObj = $null
	### line "displayname = $disp"

	$samObj = $propertyBag.Item( 'samaccountname' )
	if( $samObj -and ( $samObj.Count -gt 0 ) )
	{
		$sam = $samObj.Item( 0 )
		if( !$sam ) { $sam = "" }
	}
	else
	{
		$sam = ""
	}
	$samObj = $null
	### line "sam = $sam"

	$uacObj = $propertyBag.Item( 'useraccountcontrol' )
	if( $uacObj -and ( $uacObj.Count -gt 0 ) )
	{
		$uac = $uacObj.Item( 0 )
	}
	else
	{
		line "no uac for $sam, assumed 0x200"
		$uac = $ADS_UF_NORMAL_ACCOUNT
	}
	$uacObj = $null
	### line "uac = $uac"

	$plsObj = $propertyBag.Item( 'pwdlastset' )
	if( $plsObj -and ( $plsObj.Count -gt 0 ) )
	{
		$pls = $plsObj.Item( 0 )
	}
	else
	{
		### this can be a normal occurence if the password has never been set
		$pls = 0
	}
	$plsObj = $null
	### line "pls = $pls"

	line ( $sam.PadRight( 21 ) + $pls.ToString().PadRight( 21 ) + $uac.ToString().PadRight( 8 ) + '0x' + $uac.ToString('x').PadRight( 8 ) )

	if( $pls -eq 0 )
	{
		line ( " " * 8 + "The password has never been set for this user, skipped" )
		continue
	}

	[System.Int64]$localMaxAge = $maximumPasswordAge

	if( $domainMode -ge 3 )
	{
		###
		### PSOs can be created on Server 2003, but they don't work properly until
		### the domain mode is Windows2008Domain or higher.
		###
		### So if we find a PSO and the domain mode is Windows2008Domain or higher,
		### we first determine whether we've seen this PSO before. If we have seen
		### the PSO before, then we've stored the msDS-MaximumPasswordAge value for
		### the PSO into a hash table for quick retrieval. If we have not seen the
		### PSO before, then we use ADSI to load the PSO and retrieve the value for
		### msDS-MaximumPasswordAge and then cache the value for future access. By
		### using a cache, we only have to access Active Directory to obtain values
		### once per PSO, leading to a significant performance improvement compared
		### to using the native cmdlets or S.DS.
		###

		$psoObj = $propertyBag.Item( 'msds-psoapplied' )
		if( $psoObj -and ( $psoObj.Count -gt 0 ) )
		{
			$pso = $psoObj.Item( 0 )
			### line ( "PSO object/name" + $pso )

			if( $psoCache.Item( $pso ) )
			{
				[System.Int64]$localMaxAge = $psoCache.Item( $pso )
				### line ( " " * 8 + "Accessed PSO from cache = " + $pso )
			}
			else
			{
				$psoADSI = [ADSI]( "LDAP://" + $pso )
				$ageOBJ = $psoADSI.'msDS-MaximumPasswordAge'

				[System.Int64]$localMaxAge = $psoADSI.ConvertLargeIntegerToInt64( $ageOBJ.Value )

				### Convert to days
				### 	there are 86,400 seconds per day (24 * 60 * 60)
				### 	there are 10,000,000 nanoseconds per second
				$localMaxAge = ( -$localMaxAge / ( 86400 * 10000000 ) )

				$psoCache.$pso = $localMaxAge
				### line ( " " * 8 + "Stored PSO to cache = " + $pso )

				$ageOBJ  = $null
				$psoADSI = $null

				### line "localMaxAge = $localMaxAge"
			}
		}
		else
		{
			### completely normal to not have a PSO, in that case, use the maxPwdAge
			### from the default domain policy.
			$pso = $null
			### line "pso is null"
		}
		$psoObj = $null
	}

	### In an Exchange environment, the 'mail' attribute contains the primary SMTP
	### address for a user. In a non-Exchange environment, that should also be true,
	### but no system process validates it. We do presume that the mail address is
	### valid, if present.

	$mailObj = $propertyBag.Item( 'mail' )
	if( $mailObj -and ( $mailObj.Count -gt 0 ) )
	{
		$mail = $mailObj.Item( 0 )
	}
	else
	{
		$mail = ''
	}

	if( 0 )
	{
		### The conditions reported on below cannot occur based on our LDAP filter
		### But they helped me develop and test the LDAP filter. :-)

		if( $uac -band $ADS_UF_NORMAL_ACCOUNT )
		{
			"`t`tNormal account"
		}
		else
		{
			"`t`tNot a normal account"
		}
		if( $uac -band $ADS_UF_ACCOUNTDISABLE )
		{
			"`t`tAccount disabled"
		}
		else
		{
			"`t`tAccount enabled"
		}
		if( $uac -band $ADS_UF_DONT_EXPIRE_PASSWD )
		{
			"`t`tPassword doesn't expire"
		}
		else
		{
			"`t`tPassword expires"
		}
		if( $uac -band $ADS_UF_PASSWD_NOTREQD )
		{
			"`t`tPassword not required"
		}
		else
		{
			"`t`tPassword required"
		}
	}

	### If we get here, $pls is non-zero.
	###
	### $pls comes to us in FileTime format (the number of 100 nansecond ticks 
	### since Jan 1, 1601). So it must be converted to DateTime and adjusted 
	### for the normal clock in order for us to do our arithmetic on it. For
	### more information about FileTime, see:
	### msdn.microsoft.com/en-us/library/windows/desktop/ms724290(v=vs.85).aspx

	$date = [DateTime]$pls
	$passwordLastSet = $date.AddYears( 1600 ).ToLocalTime()
	$passwordExpires = $passwordLastSet.AddDays( $localMaxAge )

	line ( " " * 8 + "The password was last set on " + $passwordLastSet.ToString() )

	if( $now -gt $passwordExpires )
	{
		line ( " " * 8 + "The password already expired on $($passwordExpires.ToString()). No email will be sent.")
		continue
	}
	else
	{
		line ( " " * 8 + "The password will expire on "  + $passwordExpires.ToString() )
	}

	$difference = $passwordExpires - $now
 	$days       = $difference.Days.ToString()
	$hours      = $difference.Hours.ToString()
	$minutes    = $difference.Minutes.ToString()

	line ( " " * 8 + "(This is in $days days, $hours hours, $minutes minutes)" )

	if( $difference.Days -le $daysForEmail )
	{
		if( [System.String]::IsNullOrEmpty( $mail ) -and !$adminEmailOnly )
		{
			line ( " " * 8 + "Oops - user doesn't have an email address." )
			continue
		}

		if( $DontSendEmail )
		{
			line ( " " * 8 + "Oops - we aren't supposed to send an email." )
			continue
		}

		line ( " " * 8 + "This user will be sent an email for password change." )

		$hash = @{}
		$hash.To       = $adminEmail
		$hash.Priority = 'High'
		$hash.From     = $SMTPfrom

		if( ![System.String]::IsNullOrEmpty( $disp ) )
		{
			$hash.Subject = "Warning! The network password for $disp ($sam) is about to expire."
		}
		else
		{
			$hash.Subject  = "Warning! The network password for $sam is about to expire."
		}

		if( !$adminEmailOnly -and ![System.String]::IsNullOrEmpty( $mail ) )
		{
			$hash.CC = $mail
		}

		if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
		{
			$hash.SmtpServer = $SMTPserver
		}
		elseif( ![System.String]::IsNullOrEmpty( $PSEmailServer ) )
		{
			$hash.SmtpServer = $PSEmailServer
		}

		###
		### Send-MailMessage will default to using $PSEmailServer when no other SMTP server is specified.
		### We checked earlier to ensure that at least one of those was specified.
		###

		if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
		{
			###
			### If SMTPuser is specified then SMTPpassword is also specified.
			### We checked earlier to make certain that if one was specified,
			### then both were specified.
			###

			$hash.Credential = newPSCredential $SMTPuser $SMTPpassword
		}

		if( $SMTPuseSSL )
		{
			$hash.UseSSL = $true
		}

		$bodyHeader = @"
WARNING!

"@
		$bodyHeader += "`r`nFor network user id: "
		if( ![System.String]::IsNullOrEmpty( $disp ) )
		{
			$bodyHeader += $disp + " (" + $sam + ")"
		}
		else
		{
			$bodyHeader += $sam
		}

		$hash.Body = @"
$bodyHeader

Your password is about to expire in $days days, $hours hours, $minutes minutes. 

Please change it now!

Thank you,
Your System Administrator
"@

		###
		### This is V2.0 function. I should replace it with something v1.0 compatible.
		### (splatting is also V2.0 only, which is why I'm lazy and didn't replace it.)
		###

		Send-MailMessage @hash

		$hash = $null
	}
}

###
### Invidual report emails complete.
### Now send summaries to the console and/or to the administrator email address.
###

line " "

if( !$quiet )
{
	###
	### If $quiet is NOT set, then dump the report to the console, as well
	### as sending the email to the administrator.
	###
	$script:adminReport
}

if( !$DontSendEmail )
{
	$hash = @{}
	$hash.To       = $adminEmail
	$hash.Priority = 'High'
	$hash.From     = $SMTPfrom
	$hash.Subject  = "Admin Report - Send Mail to Users with Expiring Passwords - Run Date/Time: " + $now.ToString()
	$hash.Body     = $adminReport

	###
	### Send-MailMessage will default to using $PSEmailServer when no other SMTP server is specified.
	### We checked earlier to ensure that at least one of those was specified.
	###

	if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
	{
		$hash.SmtpServer = $SMTPserver
	}
	elseif( ![System.String]::IsNullOrEmpty( $PSEmailServer ) )
	{
		$hash.SmtpServer = $PSEmailServer
	}

	if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
	{
		###
		### If SMTPuser is specified then SMTPpassword is also specified.
		### We checked earlier to make certain that if one was specified,
		### then both were specified.
		###

		$hash.Credential = newPSCredential $SMTPuser $SMTPpassword
	}

	if( $SMTPuseSSL )
	{
		$hash.UseSSL = $true
	}

	Send-MailMessage @hash

	$hash = $null
}

###
### Clean up a bit.
###

$now   = $null
$users = $null

$psoCache        = $null
$directorySearch = $null
$domainADSI      = $null
$domainObject    = $null

$script:adminreport = $null

### Done.

Until next time…

If there are things you would like to see written about, please let me know.


Follow me on twitter!  – @EssentialExch

Generating a report on Distribution Groups and their Membership, v2

In August of 2010, I posted Generating a report on Distribution Groups and their Membership. For most people, that script worked just fine.

However, it had some issues:

  • Large groups would cause PowerShell to generate an error about concurrent pipelines
  • The script generated string output instead of object output

This new version fixes those issues and adds ‘help’ content for the script. The script is below, but the information about the reasoning is copied directly from the original post, modified appropriately.

A common request is to get a list of all distribution groups and the members contained in that distribution group.

Note: a distribution group may also be a security group. From an Exchange Server perspective, the important thing is whether the group is mail-enabled or not.

You can take this report and pipe it to out-file in order to save the output to a disk file. Then you can inspect the file later, email it, copy-n-paste it, whatever you want. Export-Csv and Export-CliXml are also good options for exporting this data, especially since there is the potential for the contents of several arrays to be output.

Here is the script:

##
## Report-DistributionGroupsAndMembers.ps1
## v2.0
##
## Michael B. Smith
## http://TheEssentialExchange.com
## August, 2010
## July, 2011
##
## Requires the Exchange Management Shell
## As of version 2.0, requires PowerShell 2.0
## Tested on both Exchange 2007 and Exchange 2010
##

#requires -Version 2.0

if( $args )
{
	if( ( $args.Length -eq 1 ) -and
	    ( ( $args[0] -eq "-?" ) -or
	      ( $args[0] -eq "-help" ) ) )
	{
		@"

NAME
	Report-DistributionGroupsAndMembers

SYNOPSIS
	This script outputs information about all distribution groups. The 
	information includes:

	GroupName    - The name of the distribution group
	Identity     - The unique identity of the group (an X400 identifier)
	ManagerNames - An array containing the names of the managers for the
		group. On Exchange 2007, this will contain a maximum of one
		entry. On Exchange 2010, there may be many entries. It is
		also possible (and quite likely in some environments), for
		this array to be empty.
	ManagerCanonicalNames - An array containing the canonical names
		(also known as the relative distinguished name) for the
		managers of this group. It will match, index by index, the
		contents of ManagerNames.
	Members      - An array containing the names of all the members of
		this group. It is also possible for this array to be empty.

	This script must be executed from within an Exchange Management Shell. 

SYNTAX
	Report-DistributionGroupsAndMembers [-help]

"@ 		| out-default

		return
	}

	throw "No parameters are allowed for this script"
}

$distributionGroups = Get-DistributionGroup -ResultSize Unlimited

foreach( $distributionGroup in $distributionGroups )
{
	## not all of these temporaries are necessary, but it
	## simplifies understanding the PowerShell code

	$groupName  = $distributionGroup.Name
	$groupID    = $distributionGroup.Identity

	$managedBy  = $distributionGroup.ManagedBy
	$mgrName    = @()
	$mgrCName   = @()

	if( $managedBy -is [Microsoft.Exchange.Data.Directory.ADObjectId] )
	{
		$mgrName  += $managedBy.Name
		$mgrCName += $managedBy.RDN.ToString().SubString(3)
	}
	elseif( $managedBy.Count -gt 0 )
	{
		foreach( $manager in $managedBy ) 
		{
			$mgrName  += $manager.Name
			$mgrCName += $manager.RDN.ToString().SubString(3)
		}
	}

	$membersArray = @()

	$members = Get-DistributionGroupMember -Identity $groupID -ResultSize Unlimited
	foreach( $member in $members )
	{
		$membersArray += $member.Name
	}

	$members = $null

	$hash = @{
		GroupName		= $groupName
		Identity		= $groupID
		ManagerNames		= $mgrName
		ManagerCanonicalNames	= $mgrCName
		Members			= $membersArray
	}

	New-Object PSObject -Property $hash	## inject to the pipeline
}

$distributionGroups = $null

 

Until next time…

If there are things you would like to see written about, please let me know.


Follow me on twitter! : @EssentialExch

Black Hole Distribution Groups

I had the question posed to me today: if I have a mail-enabled group (either a security group or a distribution group) that has no members, and it receives an email, why don’t I get an NDR?

Well – works as intended!

Let’s have a thought experiment considering how mail-enabled groups work (from an Exchange perspective):

  1. Create a distribution group
  2. Assign it one or more email addresses
  3. Don’t add any members
  4. Send an email
  5. Let Transport expand the group
  6. For every member in the group, make a copy of the email
  7. Stop

What’s different about the empty group? Absolutely nothing. A copy of the email is made for every member of the group – it just so happens that there aren’t any members of the group!

Why would you do this? It’s commonly done for employees that have left a company. For some period of time, you may want an ex-employees supervisor or manager to receive their emails. But eventually, you just want them to “go away”.

It’s also commonly done for “temporary” email addresses. For example, your company decides to run a promotion, and your have your customers send an email to a custom temporary email address. When the promotion is done, black-hole the email.

Now, starting in Exchange 2007 you can also create custom responses with transport rules, if you need or want them.

Black hole distribution groups have worked this way since at least Exchange 5.5. A further note: it’s also common to hide these type of mail-enabled groups from the Exchange address book, so that they are not present in the GAL. Also be aware that you can have dozens of addresses assigned to a single group; so in most instances you’ll only need a single black hole distribution group in your organization.

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter! : @EssentialExch

Exchange 2003 FE Servers vs. Server 2008 Active Directory

I spent eight hours over the last two days figuring out an interesting – but weird – problem. Once I figured out the problem, correcting it was a simple matter.

I’ll just give you the problem description and then the solution. Hopefully it will save you more than a little time in the future.

I have a client (one of several in this particular situation) who has an Exchange 2003 Front-End server located in their DMZ. Yes, that’s right, their DMZ. In the “long-ago time”, this was considered to be a favored practice by Microsoft. That, of course, has changed over the years. Now, in 2011, we do not want any domain member servers located in the DMZ. Why is that? Because to install a domain member in a DMZ basically means that you have to make “swiss cheese” out of your firewall. And, effectively, if a DMZ server is compromised it means that your entire Active Directory can be compromised. The entire list of potentially affected ports is shown in KB 832017, and at the end of the day – it basically says that you have to open everything above port 1024 to make it all work.

With all that being said, if you are using Exchange 2003 with Server 2003 Active Directory (by which I mean an Active Directory domain controller hosted on a Windows Server 2003 server), you could get by with a much more limited set of ports (although it still isn’t small!):

DNS (TCP and UDP 53)
Kerberos (TCP and UDP 88)
RPC Endpoint Mapper (TCP 135)
NetBIOS Name Service (TCP 137)
NetBIOS Datagram Service (TCP 138)
NetBIOS Session Service (TCP 139)
LDAP (TCP and UDP 389)
Directory Services (TCP 445)
LDAP Global Catalog (TCP 3268)
Remote Desktop Protocol (TCP 3389)
Active Directory RPC End-Point (AD-RPC-EP)

You should notice that all of these have defined ports except for the last! By definition, the AD-RPC-EP is a random high port.

However, a somewhat surprising fact is that when you run Active Directory on Windows Server 2003 (in other words, you have a domain controller on Server 2003), the AD-RPC-EP is always either port 1025 or 1026!

Starting with Server 2008, this is no longer true. “Port randomization” ensures that ports are allocated at truly random locations.

So… I have a particular client who, as part of their Exchange 2003 to Exchange 2010 upgrade effort decided that they first wanted to upgrade their Active Directory. That was fine by me – there are no documented restrictions regarding the operating system level of domain controllers to be used by Exchange 2003.

The day we changed the last domain controller from Windows Server 2003 to Windows Server 2008 R2, the Exchange FE server stopped working. Oh, $%&#.

Thankfully, the FE server was only being used for Outlook Web Access (OWA). SMTP was injected into the Exchange environment via a Barracuda cluster directly into the internal back-end server environment.

I’m rather hardheaded. So I wanted to figure this out. And, quite frankly, it took awhile. Eventually, it took an examination using portqry and rpcping (you can find out lots of information about these utilities by using Google or Bing and searching for “portqry site:support.microsoft.com” and “rpcping site:support.microsoft.com”). Comparing the results of those to the “access control lists” from the firewall showed me that the firewall always expected a port to be open at TCP 1025 and/or TCP 1026. Neither of these ports were open on Server 2008 R2!

I went back and investigate some other customers who were running Active Directory on Server 2003. On every single one of their servers, the process lsass.exe had either TCP 1025 or TCP 1026 (or both) open.

A google here, a bing there, and I was led to Active Directory Replication Over Firewalls and KB 224196: Restricting Active Directory replication traffic and client RPC traffic to a specific port.

These led me to understand that on Server 2008 and above AD-RPC-EP could happen at any random port – but that there is a way to specify the port that will be used. YAY!

For the following

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters
Registry value: TCP/IP Port
Value type: REG_DWORD
Value data: (available port)

specifying a “value data” of either 1025 or 1026 ensures that Server 2008 and Server 2008 R2 operate as did Server 2003. After setting this value, it does take a reboot of the affected DC/server for the value to take effect.

Once this was done, my client was happy again! I hope this helps you in your migrations…

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter! : @EssentialExch

More on Active Directory Privileged Groups and Exchange Server

In October of 2008, I wrote the article adminCount, adminSDHolder, SDProp and you! This article discussed why membership in privileged groups could cause permissions challenges. You may want to refer to it before proceeding.

With Exchange Server 2010, the overall situation has continued to get more restrictive. Exchange Server 2010 needs for users who have mailboxes to inherit permissions in order for Role-Based Access Control (RBAC) and overall Exchange access to work properly. Membership in a privileged group will stop a mailbox from inheriting permissions. Usually, normal mailbox access will work fine (i.e., using Outlook with MAPI) due to historical permissions present on a mailbox, but any other IIS based or MAPI service will fail. This includes, but is not limited to, Blackberry Enterprise Server, Exchange ActiveSync, Outlook Web App, etc.

You can see a complex way of working around this issue described in KB Article 907434: The “Send As” right is removed from a user object after you configure the “Send As” right in the Active Directory Users and Computers snap-in in Exchange Server.But that method is an administrative nightmare.

Realistically speaking – what can you do?

The answer is simple: Get your users out of privileged groups.

No user who is Administrator, a Domain Admin, an Enterprise Admin, or a Schema Admin should be signing into Exchange with an account that is a member of those groups. This is also true for any member of the Built-in\Administrators group that exists on Domain Controllers. Every user who has access to a high-privilege account should also have a normal user account that they use for day-to-day usage – just like everyone else.

For the other privileged groups (Account Operators, Server Operators, Print Operators, Backup Operators, Cert Publishers) – these are legacy groups. They are a carry-over from Windows NT. Don’t use them. Instead, use a computer’s Local Security Policy – or a Domain Security Policy when appropriate – to assign users who need those capabilities the specific rights they require.

Usually, you will find that the built-in groups provide more power than you think they do (e.g., a member of Account Operators, Server Operators, Print Operators, or Backup Operators can log in locally to a domain controller and shut it down). Mapping between the groups is fairly simple by examing User Rights Assignments in any Local Security Policy. An online version of this is available at: http://technet.microsoft.com/en-us/library/bb726980.aspx in tables 7-7, 7-8, and 7-9.

You may not normally consider <insert-any-group-name-here> to be a privileged group. But Active Directory does. Get users out of privileged groups where possible, and where not possible – assign (and require use of!) users a normal account and a privileged account.

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter! : @EssentialExch

Generating a report on Distribution Groups and their Membership

A common request is to get a list of all distribution groups and the members contained in that distribution group. The question came up on a mailing list I frequent today, and my initial response was a PowerShell “one-liner” (actually five lines, but all a single PowerShell statement).

The one-liner worked, but it had a couple of limitations: it wasn’t very pretty (that is, the output was formatted poorly) and if there were than one user set to manage the distribution group, that would be reportedly incorrectly. It was also slower than it had to be.

Note: a distribution group may also be a security group. From an Exchange Server perspective, the important thing is whether the group is mail-enabled or not.

So, I took that five-liner, cleaned it up, fixed the ManagedBy reporting bug, and sped it up (by using an embedded pipeline to report on the members contained in a distribution group). That turned it into a 50+ line script (which includes 10 lines of comments!). As I said on the mailing list – you can generate quick results with PowerShell. That’s good enough for most admins. But if you want it pretty and “production quality” then it’s going to take a little more time.

You can take this report and pipe it to out-string in order to save the output to a disk file. Then you can inspect the file later, email it, copy-n-paste it, whatever you want.

Here is the script:

 

##
## Report-DistributionGroupsAndMembers.ps1
## v1.1

##
## Michael B. Smith
## http://TheEssentialExchange.com
## August, 2010
##
## Requires Exchange Management Shell
## Should work with either PowerShell v1 or v2
## Tested on both Exchange 2007 and Exchange 2010

##

function formatManager($formatstring, $manager)
{
	$formatstring -f $manager.Name, ($manager.Parent.ToString() + "/" + $manager.RDN.ToString().SubString(3))
}

Get-DistributionGroup -ResultSize Unlimited |% {
	$group = $_
	"Group Name & Identity: {0}, {1}" -f $group.Name, $group.Identity
	$managedBy = $group.ManagedBy
	if( $managedBy -is [Microsoft.Exchange.Data.Directory.ADObjectId] )
	{
		formatManager "Group manager: {0}, {1}" $managedBy
	}
	elseif( $managedBy.Count -gt 1 )
	{
		[bool]$first = $true
		foreach( $manager in $managedBy ) 
		{
			if( $first ) 
			{
				formatManager "Group managers: {0}, {1}" $manager
				$first = $false
			}
			else
			{
				formatManager "                {0}, {1}" $manager
			}
		}
	}
	elseif( $managedBy.Count -gt 0 )
	{
		formatManager "Group manager: {0}, {1}" $managedBy[0]
	}

	"Members:"
	Get-DistributionGroupMember -Identity $group.Identity -ResultSize Unlimited |% {
		foreach( $member in $_ )
		{
			"`t$($member.Name)"
		}
	}
	"---"
}

 

Until next time…

If there are things you would like to see written about, please let me know.

–Edit on August 20, 2010
The original script would not display ManagedBy on Exchange 2007 (I’m running Exchange 2010). This is because ManagedBy in Exchange 2010 always returns an array-type object (it can contain multiple users). In Exchange 2007, the ManagedBy value is always a singleton. The required changes to the script are in red.

Michael B.


Follow me on twitter: @EssentialExch

Exchange Email Address Template Objects in PowerShell

Exchange has a very rich set of objects which are used and created by the various Exchange cmdlets. Unfortunately, these objects (excepting those used by Exchange Web Services) are poorly documented – and the documentation which is available is often incorrect or misleading.

I ran into that problem this past week.

To understand my particular issue, let’s review a little history. Exchange 2000 and Exchange 2003 didn’t have the concept of individual “accepted domains”. Instead, you usually used Recipient Policies to identify the accepted domains. Recipient Policies were also used to create and manage Mailbox Manager policies.

In Exchange 2007, all of the capabilities of Recipient Policies were split into multiple features: Accepted Domains, E-mail Address Policies, and Managed Folder Mailbox policies. In Exchange 2010, Managed Folder Mailbox policies have been deprecated and replaced by Retention Policies.

However, some capabilities of these features are not available in the Exchange 2007/2010 GUI (but were in the Exchange 2000/2003 GUI). One of these hidden features is to have both enabled and disabled address templates in an E-Mail Address Policy. In the case of a disabled address template, a particular e-mail address domain can be manually assigned to a given Exchange object but that e-mail address domain will not be automatically assigned even if the object otherwise meets the requirements of the recipient filter assigned to the E-Mail Address Policy (such as a company name, a conditional attribute, group membership, etc.).

Whew.

So what happens when you need to disable an existing e-mail address template?

Well, you do it in PowerShell. (Long lead up to an obvious response, right?) And you do it with the obvious PowerShell cmdlet: Set-EmailAddressPolicy.

What would seem to make sense that the command would be something like:

Set-EmailAddressPolicy ‘Default Policy’ -Add -DisabledEmailAddressTemplates ‘@example.com’

…but that doesn’t work. Aside from the fact that there is no Add or Remove parameter to Set-EmailAddressPolicy, you will eventually figure out that whatever value is provided to the EnabledEmailAddressTemplates and the DisabledEmailAddressTemplates parameters overwrite the values previously there. Well, ain’t that a kick?

To change an address template collection, you must modify the existing collection and resubmit the modified value to Set-EmailAddressPolicy.

So what’s the problem? The problem is the type of the required argument. The EnabledEmailAddressTemplates and DisabledEmailAddressTemplates parameters take a parameter of type Microsoft.Exchange.Data.ProxyAddressTemplateCollection. This is an enumerated collection (a fancy name for an array that you can use with a foreach(), or index it, or use the Item() method to access elements of the collection). Individual items of the collection are of type Microsoft.Exchange.Data.ProxyAddressTemplate or Microsoft.Exchange.Data.SmtpProxyAddressTemplate.

To add or remove items from the collection, you use the normal methods available to collections – that is, the Add() and Remove() methods. However, to use the Add() or Remove() methods, you need to have an object of type Microsoft.Exchange.Data.ProxyAddressTemplate or of type Microsoft.Exchange.Data.SmtpProxyAddressTemplate.

How the heck do you get one of those?

In general, with .Net objects, you create objects using a constructor. In PowerShell, that translates to the New-Object cmdlet. So my normal process is to begin by using Google/Bing to search for the object name. The first or second hit is usually the MSDN web page that describes the class. Then, I read the page, find the appropriate constructor and bam! I am done. This is the method I followed in an earlier article: More Multi-valued Parameters in PowerShell (SourceTransportServers in Set-SendConnector).

But it doesn’t always work. I refer you back to the first paragraph. Many Exchange objects are poorly documented and the documentation that is present is often wrong. As an exercise, I invite you to Google/Bing the objects we are interested in today: Microsoft.Exchange.Data.ProxyAddressTemplate or Microsoft.Exchange.Data.SmtpProxyAddressTemplate. Read the MSDN web pages for the class, especially the page on constructors.

Once you’ve done that reading, you’ll try this:

$template = New-Object Microsoft.Exchange.Data.ProxyAddressTemplate(‘smtp’, ‘@example.com’)

which will not work. Then you’ll try:

$template = New-Object Microsoft.Exchange.Data.ProxyAddressTemplate(‘smtp:@example.com’)

which also will not work. Then you’ll try to get a raw object:

$template = New-Object Microsoft.Exchange.Data.ProxyAddressTemplate

which again will not work.

Then, if you are like me, you’ll spend the next couple of hours banging your head against the wall getting nowhere. After sleeping on the problem, I did come up with a very unsatisfying work-around. Since, in this case, we are trying to take a “currently enabled” object and make it disabled, we should be able to use a reference to the existing object. That solution looks like the below:

	$template = '@example.com'

	$policy = Get-EmailAddressPolicy 'Default Policy'

	$EnabledAddresses  = $policy.EnabledEmailAddressTemplates
	$DisabledAddresses = $policy.DisabledEmailAddressTemplates

	$objTemplate = $null

	foreach ($addr in $EnabledAddresses)
	{
		if ( $addr.AddressTemplateString -ieq $template )
		{
			"found $template"
			$objTemplate = $addr
			break
		}
		"skipped " + $addr.AddressTemplateString
	}

	if ( $objTemplate )
	{
		$EnabledAddresses.Remove( $objTemplate )
		$DisabledAddresses.Add( $objTemplate )

		$policy |
			Set-EmailAddressPolicy -EnabledEmailAddressTemplates $EnabledAddresses `
					       -DisabledEmailAddressTemplates $DisabledAddresses
	}

That, however, is a kludgey hack. In the above solution, we are using a reference to an existing object to change the policies. That will not work in many use-cases; for example where you want to add a new address template, whether enabled or disabled. You will find that you can’t modify the objects returned from Get-EmailAddressPolicy – they are marked read-only.

So, I asked some people who know a lot more about PowerShell than I. Shay Levy, a PowerShell MVP, pointed out that I was making it too hard and that this works:

$objTemplate = [Microsoft.Exchange.Data.ProxyAddressTemplate]’smtp:@example.com’

So, I slapped myself upside the head and said “Of course! ::Parse() is also a hidden constructor!” What that means, in English, is that the above statement is equivalent to:

$objTemplate = [Microsoft.Exchange.Data.ProxyAddressTemplate]::Parse( ‘smtp:@example.com’ )

and to:

$objTemplate = ‘smtp:@example.com’ -as [Microsoft.Exchange.Data.ProxyAddressTemplate]

PowerShell has a built-in capability, using a static method on an object, to take a string and attempt to coerce that string into the named object. In this case, a Microsoft.Exchange.Data.ProxyAddressTemplate. That static method must be named Parse() for PowerShell to automatically use the capability. You can see the static methods on this class using the below command:

[Microsoft.Exchange.Data.ProxyAddressTemplate] | Get-Member -static

Oisin Grehan, another PowerShell MVP, also pointed out that this is a perfect opportunity to use a filter. A filter is a PowerShell feature, somewhat uncommonly used, for processing objects in the pipeline. For example, to define a couple of useful filters:

	filter ConvertTo-ProxyAddressTemplate
	{
		$_ -as [Microsoft.Exchange.Data.ProxyAddressTemplate]
	}

	filter ConvertTo-SmtpProxyAddressTemplate
	{
		$_ -as [Microsoft.Exchange.Data.SmtpProxyAddressTemplate]
	}

These filters would take input from the pipeline and convert the pipeline input into the desired objects. For example:

	[PS] C:\S>'@example.com', '@sub1.example.com', '@sub2.example.com' | ConvertTo-SmtpProxyAddressTemplate

	AddressTemplateString      : @example.com
	ProxyAddressTemplateString : smtp:@example.com
	Prefix                     : SMTP
	IsPrimaryAddress           : False
	PrefixString               : smtp

	AddressTemplateString      : @sub1.example.com
	ProxyAddressTemplateString : smtp:@sub1.example.com
	Prefix                     : SMTP
	IsPrimaryAddress           : False
	PrefixString               : smtp

	AddressTemplateString      : @sub2.example.com
	ProxyAddressTemplateString : smtp:@sub2.example.com
	Prefix                     : SMTP
	IsPrimaryAddress           : False
	PrefixString               : smtp

So, given what we know now, what does the script look like? There are some similarities, but now it doesn’t feel like a hack and it is much easier to read and understand:

	$objTemplate = '@example.com' | ConvertTo-SmtpProxyAddressTemplate

	$policy = Get-EmailAddressPolicy 'Default Policy'

	$EnabledAddresses  = $policy.EnabledEmailAddressTemplates
	$DisabledAddresses = $policy.DisabledEmailAddressTemplates

	$EnabledAddresses.Remove( $objTemplate )
	$DisabledAddresses.Add( $objTemplate )

	$policy |
		Set-EmailAddressPolicy -EnabledEmailAddressTemplates $EnabledAddresses `
				       -DisabledEmailAddressTemplates $DisabledAddresses

So, what is the lesson learned? Don’t forget about ::Parse()! And, as almost always, in PowerShell there is often more than one way to skin a cat.

Until next time…

If there are things you would like to see written about, please let me know.


Follow me on twitter: @EssentialExch