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

September 2017 Quarterly Exchange Updates

Microsoft has released Exchange Server 2016 CU7 (download) and Exchange Server 2013 CU18 (download) for on-premises servers today.

Perhaps most noticeable is that Exchange Server 2016 CU7 now enforces a Forest Functional Level (and therefore a Domain Functional Level) of Server 2008R2. This change had been announced some time ago, but now it is enforced.

The list of documented fixes in Exchange Server 2016 CU7 is small, but one of the fixes is an annoyance (a warning that can’t be eliminated) associated with UseDatabaseQuotaDefaults when using Set-Mailbox. I’m glad to see it fixed!

Exchange Server 2016 CU7 and Exchange Server 2013 CU18 share another fix which can cause a corrupted email attachment if the attachment is exactly a certain size.

Exchange Server 2013 CU18 also includes various Daylight Savings Time updates for countries all over the world.

Please remember a few things:

You should always test in a lab first.

Your installation of a CU may fail or take significantly longer if you don’t disable anti-virus and anti-malware software before the installation.

If you have a large number of servers, you should probably drain and place each server in maintenance mode before applying the CU (and then return them to operational mode after!).

I generally find that things go more smoothly if you reboot your server “very first thing”.

Not every CU may contain changes to the Active Directory Schema, or to RBAC roles, or many other things. But life can often be made simpler by doing a PrepareSchema and a PrepareAllDomains before executing the upgrade. On my first server to be upgraded, my normal process is this:

setup /IAcceptExchangeServerLicenseTerms /PrepareSchema
setup /IAcceptExchangeServerLicenseTerms /PrepareAllDomains
setup /IAcceptExchangeServerLicenseTerms /m:upgrade

Use an elevated cmd.exe session, not a PowerShell session. (PowerShell searches the path differently than cmd.exe – PowerShell will find the setup.exe in $exbin instead of the setup.exe in the current folder.)

After the upgrade, you should again reboot. Then re-enable your anti-virus and anti-malware. Finally, place the server back in operational mode.

Happy upgrading!


Please follow me on twitter! @EssentialExch

iOS 11 about to release – Things to be aware of

Apple has announced that iOS 11 will be released tomorrow (September 19, 2017). If you are a Windows admin, there are probably some things about which you should be aware. This is not intended to be an exhaustive list, but does describe some rather important items…

  1. Exchange ActiveSync is broken when you run Exchange Server 2016 on Windows Server 2016. Apple is aware of the issue and is pursuing a fix. On a technical level, this happens because iOS 11 is improperly negotiating a HTTP/2 TLS connection and the connection fails. Microsoft has a fully supported workaround, which disables HTTP/2 TLS connections.

    The workaround is described in this article: How to deploy custom cipher suite ordering in Windows Server 2016.

    This only occurs with iOS 11 and Windows Server 2016, because in earlier versions of each, HTTP/1.1 was the default, not HTTP/2.

    (If you think the article name is weird, well so do I.)

  2. The default picture format for iPhones 7/8/X is changing. As a Microsoft employee wrote earlier today:

    The new photo and video formats result in files about 1/2 size of the old JPEG and video formats, while having better quality. The problem is that new files will likely not open properly outside of your phone until everything that you use to work with photos updates to work with new HEIF formats.

    To check if your iOS 11 phone uses the new format, go to Settings > Camera > Formats. “High Efficiency” is new and “Most Compatible” is the old / current.

    I do not suggest to just turn this off; hey – getting files half the size is super cool. Just realize that if you use the photos outside of your phone that there might be temporary issues with viewing.

    Windows and OneDrive do not yet support the new formats.

    h/t ninob

  3. Yammer and Dynamics CRM apps have not yet been updated for iOS 11.

    There are a wide variety of Intune changes/impacts with iOS 11:

    Support Tip: Intune Support for iOS 11

    Perhaps the two things most notable to your users: [3a] An updated Company Portal and Managed Browser are required for iOS 11, and [3b] Drag-and-drop (a new feature of iOS 11) is disabled when a device is enrolled with Intune.

    h/t briand


That’s all for now. Please 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:\> 

 

Forcing a Server’s Active Directory Site

In January 2010 I wrote a blog post Where oh where, did my AD site go…[Alternate title: It’s the DNS, stupid.]. In that blog post I discussed a situation where an incorrect DC locator record could cause a server to report itself as a member of an improper Active Directory site. That can cause a number of issues with Exchange.

I am in the process of migrating that same customer to Exchange 2013 (the prior blog post was written when migrating a particular customer to Exchange 2010).

The first Exchange 2013 server was brought online after the OS was installed. I went through the normal process of installing Exchange 2013 role and feature pre-requisites, installed Ucma 4.0, etc. etc. When it came time to do the first actual step in installing Exchange 2013, PrepareSchema, setup.exe reported that the Schema Master FSMO was not in the same Active Directory site as the computer running setup.

Huh?

Of course it was. I know this requirement and made certain it was satisfied! The Schema Master FSMO was in the AD site named “10-129-59”. The new server was in the same subnet.

However, when executing “nltest /dsgetsite”, nltest reported that the AD site was “Default-First-Site-Name”. Uh, wow.

I immediately reviewed AD Sites and Services to ensure that AD Subnets and AD Sites were properly configured. Indeed, they were. Next, I reviewed the customer’s DNS, in detail, as described in the above blog post. The DNS was correct.

Finally, with little hope of success, I tried resetting the secure channel to the proper FSMO DC. That succeeded.

So, I rebooted. After the reboot, the secure channel was again reset to a DC in “Default-First-Site-Name”. OK, I tried the same thing again (resetting the secure channel and then rebooting) with no change in behavior.

No need to try a third time. That would meet a classical definition of insanity. 🙂

I spent a limited amount of time investigating the particular reasons for why this should occur. But when it comes down to it, as a consultant, my job is to accomplish this project. So, I went out to find ways to ensure that a particular computer is a member of a particular AD site.

It turns out to be pretty simple. You must set a registry value for this key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters

The value is called SiteName and is of type REG_SZ (the name is case sensitive).

In my case, I set SiteName to “10-129-59” and closed regedit.exe (of course you can set this value in many ways – you can use PowerShell, .NET, Win32, reg.exe – whatever you wish to use). Documentation says that restarting the NetLogon service should correct everything, but that is not my experience. After rebooting the server, the computer came up in the proper AD site and I was able to proceed with installing Exchange Server 2013.

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

 

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

 

Exchange Server 2013 Service Pack 1 Released!

Just a quick note…. Exchange Server 2013 Service Pack 1 has been released.

Among other changes, this version of Exchange Server provides support for installation on Windows Server 2012 R2 and provides support for Windows Server 2012 R2 domain controllers.

The blog post announcing the release is here. You can download the release here.

At the same time were releases for two legacy versions of Exchange: Update Rollup 5 for Exchange 2010 Service Pack 3 and Update Rollup 13 for Exchange 2007 Service Pack 3.

The announcement for those releases is here. At the time of this writing, no information is available about the contents of those rollups.

More information coming soon!

Follow me on twitter: @essentialexchange

 

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