Collect and email Exchange Server stats automatically using PowerShell

Here’s a quick and dirty way to monitor and keep a record of some basic server statistics even when you’re not in the office. I’m sure there are many very nice tools out there that can do the same thing, but this one is free!

And who doesn’t like free?

This script gathers the following performance counters from my Exchange servers and one domain controller and then emails them to me in a nice, neat table:

  • \Processor(_Total)\% Processor Time
  • \LogicalDisk(C:)\Free Megabytes
  • \Memory\Available Mbytes
  • \MSExchangeTransport Queues(_total)\Submission Queue Length
  • \MSExchangeIS\Active User Count

An array would be probably a better way to feed the server names into the GetStats function. Maybe I’ll get around to setting it up that way someday. (Update 3/21/2013: Done! Corrected script is below.) In order to get useful statistics for CPU and memory usage, the script takes just over 10 minutes per server. What I have here isn’t perfect, but it works. Feel free to recommend changes or additions in the comments.

Here’s how you use this:

  1. Create or select an account that has permissions to read performance counters on your target servers.
  2. Create a folder to store the message body. I created a folder under “C:\Temp\” called Tasks. Make sure the account that will be running this script has at least Modify permissions to this folder.
  3. Modify this script to suit your own environment according to the instructions (in green).
  4. Copy the script to “C:\Windows\System32” and create a scheduled task to run it at least once per day. I run mine twice during the highest usage periods around 9:30 and 3:30.


# Filename: Get_ServerStats.ps1
# Author: Jay Carper (
# Version: 13.3.21
# Purpose: Gathers statistics on Exchange servers and emails them.
# ***Change the value of $ServerConfig as indicated: ***
# 1 – If you only have one Exchange server with all roles, change the value of
# $ServerConfig to 1.
# 2 – If you have combined the CAS and Hub roles together on any number of servers
# but keep the Mailbox role separate, change the value of $ServerConfig to 2.
# 3 – If you have CAS, Hub, and Mailbox roles running on separate servers, change
# the value of ServerConfig to 3.
# 4 – If you have some other configuration, sorry, I didn’t include it. Feel
# free to make whatever changes you require to this script.
# ***Enter the name of the global catalog server that you want to monitor***
# Replace “DomCon01” below with the name of the GC DC that your Exchange
# installation primarily relies on. There could be other servers, but this
# script will only monitor one DC. If you don’t want to monitor your GC,
# set $GlobalCatalog to $Null.
# ***Enter the required information for sending the final email***
# -Replace “” with the email address that you want the email to
# come from.
# -Replace “” with the email address of the mailbox that will
# receive the results.
# -Replace “” with the name of your smtp server.

$ServerConfig = 1
$GlobalCatalog = “DomCon01”
$EmailFrom = “”
$EmailTo = “”
$SmtpServer = “”

# Defines variables for each of the counters to be reported
$lblCpu = “\Processor(_Total)\% Processor Time”
$lblDisk = “\LogicalDisk(C:)\Free Megabytes”
$lblMemory = “\Memory\Available Mbytes”
$lblQueue = “\MSExchangeTransport Queues(_total)\Submission Queue Length”
$lblUsers = “\MSExchangeIS\Active User Count”
$Output = “c:\Program Files\Tasks\ServerReport.txt”
$Today = get-date -Format yyyy-MM-dd
$Time = Get-Date -Format hh:mm

# Loads Exchange 2010 Powershell snapin if it isn’t already loaded.
if ((get-pssnapin -name Microsoft.Exchange.Management.PowerShell.E2010 -erroraction `
silentlycontinue) -eq $null) {add-pssnapin `

# HTML tags to open the body of the email.
“<html>” | out-file $Output
“<body>” | out-file $Output -append

# Body text, table tags, and column headers for the output.
“<p><strong>Messaging Server Stats for $Today $Time </strong></p><table border=1`
cellspacing=1><tr><td><strong>Server Name</strong></td><td><strong>CPU time`
</strong></td><td><strong>CPU Usage %</strong></td><td><strong>HD Time`
</strong></td><td><strong>Free HD GB</strong></td><td><strong>RAM Time`
</strong></td><td><strong>Free RAM GB</strong></td><td><strong>Spec. Time`
</strong></td><td><strong>Special</strong></td></tr>” | out-file $Output -append

# Gets counter statistics from the given server and outputs it in an html table.
function GetStats {
“<tr><td><strong>$Server</strong></td>” | out-file $Output -append

# CPU usage. A single sample is useless, so I set it to take 10 samples, 30
# seconds apart and average the values together. This is still a snapshot of
# only 10 minutes, but it’s better than a split second.
$CpuStat = “{0:N2}” -f (((Get-Counter -Counter $lblCpu -ComputerName $server `
-SampleInterval 30 -MaxSamples 10).CounterSamples | Measure-Object -Property `
CookedValue -Ave).Average)
$Time = Get-Date -Format hh:mm
“<td>$Time</td><td>$CpuStat % </td>” | out-file $Output -append

# Free Disk Space
[int]$DiskStat = ((Get-Counter -Counter $lblDisk -ComputerName `
$server).CounterSamples | foreach {$_.CookedValue})/1024
$Time = Get-Date -Format hh:mm
“<td>$Time</td><td>$DiskStat GB </td>” | out-file $Output -append

# Memory usage. See my comment about sampling CPU usage.
[single]$MemStat = “{0:N2}” -f (((Get-Counter -Counter $lblMemory -ComputerName `
$server -SampleInterval 30 -MaxSamples 10).CounterSamples | Measure-Object `
-Property CookedValue -Ave).Average/1024)
$Time = Get-Date -Format hh:mm
“<td>$Time </td><td> $Memstat GB </td>” | out-file $Output -append

# Submission queues on the hub transport servers.
if ($ServerType -eq “Hub”) {
$Queue = ((Get-Counter -Counter $lblQueue -ComputerName `
$server).CounterSamples | foreach {$_.CookedValue})
$Time = Get-Date -Format hh:mm
“<td>$Time</td><td>Queue Length $Queue </td>” | out-file $Output -append

# Mailbox user count.
if ($ServerType -eq “Mbx”) {
$Users = ((Get-Counter -Counter $lblUsers -ComputerName `
$server).CounterSamples | foreach {$_.CookedValue})
$Time = Get-Date -Format hh:mm
“<td>$Time</td><td>Users $Users </td>” | out-file $Output -append

# Submission queues and mailbox user count on a combined server. Puts mailbox
# user count on a second row.
if ($ServerType -eq “Combo”) {
$Queue = ((Get-Counter -Counter $lblQueue `
-ComputerName $server).CounterSamples | foreach {$_.CookedValue})
$Time = Get-Date -Format hh:mm
“<td>$Time</td><td>Queue Length $Queue </td></tr>” | out-file $Output -append
“<tr><td colspan=7></td>” | out-file $Output -append
$Users = ((Get-Counter -Counter $lblUsers -ComputerName `
$server).CounterSamples | foreach {$_.CookedValue})
$Time = Get-Date -Format hh:mm
“<td>$Time</td><td>Users $Users </td>” | out-file $Output -append

# Domain Controller. I didn’t have a unique counter I wanted for the DC, so I
# inserted two empty columns.
if ($ServerType -eq “DC” -or $ServerType -eq “Cas”) {“<td></td><td></td>” | `
out-file $Output -append}

# HTML tag to close the table row.
“</tr>” | out-file $Output -append

# Gather hub server info and call GetStats
function GetHubServers {
$ServerType = “Hub”
[array]$ServerNames = @()
$ServerNames = Get-TransportServer
$ServerCount = 0
foreach ($Name in $ServerNames){
[string]$Server = $ServerNames[$ServerCount].Name
$ServerCount = $ServerCount + 1

# Gather cas server info and call GetStats
function GetCasServers {
$ServerType = “Cas”
[array]$ServerNames = @()
$ServerNames = Get-ClientAccessServer
$ServerCount = 0
foreach ($Name in $ServerNames){
[string]$Server = $ServerNames[$ServerCount].Name
$ServerCount = $ServerCount + 1

# Gather mailbox server info and call GetStats
function GetMbxServers {
$ServerType = “Mbx”
[array]$ServerNames = @()
$ServerNames = Get-MailboxServer
$ServerCount = 0
foreach ($Name in $ServerNames){
[string]$Server = $ServerNames[$ServerCount].Name
$ServerCount = $ServerCount + 1

# Gets the server info and calls GetStats for organizations with a single Exchange
# server.
if ($ServerConfig -eq 1) {
[array]$ServerNames = @()
$ServerNames = Get-TransportServer
[string]$Server = $ServerNames[0].Name
$ServerType = “Combo”

# Gets the server info and calls GetStats for organizations that combine the CAS and
# Hub roles.
elseif ($ServerConfig -eq 2) {

# Gets the server info and calls GetStats for organizations that have separate
# servers for CAS, Hub, and Mailbox roles.
elseif ($ServerConfig -eq 3) {

# If $GlobalCatalog is not $Null, calls GetStats for the GC.
if ($GlobalCatalog -ne $Null) {
$ServerType = “DC”
$Server = $GlobalCatalog

# HTML tags to close the body of the email.
“</table>” | out-file $Output -append
“</body>” | out-file $Output -append
“</html>” | out-file $Output -append

# Send an email to the Exchange admin with the results.
$EmailSubject = “Messaging Server Statistics $Today $Time”
$EmailBody = get-content “C:\Program Files\Tasks\ServerReport.txt” | out-string
Send-MailMessage -From $EmailFrom -To $EmailTo -SmtpServer $SmtpServer -Subject `
$EmailSubject -BodyAsHtml $EmailBody


Do you know what the hardest part about writing these kinds of posts is? Getting all the HTML formatting right so the code looks correct in WordPress. It frequently removes line feeds and converts the characters I intended to display into the characters it thinks I intended to display. It’s possible WordPress has been taking cues from Microsoft on that point. I thoroughly tested this script, so I’m going to blame all mistakes on WordPress. (Just kidding! Even after testing, it’s possible that I made some mistakes of my own.) If you see anything that looks wrong, please let me know.

Leave a Reply

Your email address will not be published. Required fields are marked *