Exchange Management with EMS: Turning Actions into Scripts

Exchange Management Shell lets you construct Windows PowerShell scripts for common Exchange Server tasks that you'll need to run again and again.

Executive Summary:
Microsoft Exchange Server 2007's Exchange Management Shell lets you construct Windows PowerShell scripts for common Exchange Server tasks that you'll need to run again and again. For evaluating values or conditions in scripts, you use relational operators and logical operators along with code such as if statements or switch statements. To loop through a block of code in your scripts, you use code such as the for statement, foreach statement, or while statement.

Microsoft Exchange Server 2007's Exchange Management Shell (EMS) is a good interactive environment: You enter commands, the shell checks them for syntax and executes them if possible, and you see the output immediately. That's a fine model when you need to make a series of changes once or if you just need to bang out a couple of commands quickly to make something happen. However, there are many circumstances where you really want to take a group of commands, glue them together with some logic that makes decisions or evaluates conditions, and save the resulting script so you can run it at any time.

In previous articles in this series, I've shown the basics of how to use Windows PowerShell commands through EMS to retrieve Exchange Server objects, inspect their properties, and make changes to one or more objects. (See the Learning Path on the right side of the page for the other articles in this series as well as additional articles on PowerShell and Exchange management with PowerShell.) Everything we've done so far has focused on entering commands in EMS and having them immediately executed. In this article, I'll show you how to construct scripts, which you can run over and over again to automate common tasks such as getting mailbox information, creating users, and updating configuration settings across multiple servers.

Bear in mind that we're still talking about Exchange Server 2007, which uses PowerShell 1.0. PowerShell 2.0, now available as a Community Technology Preview (CTP) from Microsoft's website, includes a GUI editor that simplifies some aspects of scripting, but you can't use it just yet with Exchange.

Evaluating Conditions and Making Decisions
The interactive EMS examples I've presented in previous articles rely on the basic 3-step pattern of getting objects, filtering for the ones that should be acted upon, then taking some kind of action. One of the key differences between interactive commands and scripts is that scripts usually need a way to evaluate conditions and take actions based on the results of those evaluations.

Scripting languages typically provide relational operators that let you compare two values to see whether one is greater than, less than, or equal to the other. EMS has a range of operators, as Table 1 shows, that you can use for comparisons. These operators work on most built-in data types, including strings. However, string comparisons in EMS aren't case-insensitive by default. If you need case-sensitive comparisons, you can get them by prefixing the operator you want with the letter c. For example,

"Paul" -ceq "paul" 

returns false because the two strings aren't exactly equal.

You can also use logical operators for comparisons or to change the meaning of other types of operations. These operators, shown in Table 2, are commonly used in conditional statements where a different action is taken depending on the result. There are other, more advanced logical operators as well, but the ones here should cover most needs.

You have several ways to use conditional operators in EMS to enable your scripts to evaluate conditions and take actions on the results. The first, and simplest, method is the if statement. This procedure will be immediately familiar to anyone who's written any kind of script before: You specify a condition to test and an action to take. You can also include an else clause, which will be evaluated only if the initial condition is false. Here's an example:

If (True)
  Write-Host "PowerShell is AWESOME!"
  Write-Host "You'll never see this message."

You can also make use of the elseif statement to chain together multiple tests:

$detroitLions = FALSE;
$newOrleansSaints = TRUE;
If ($detroitLions)
  Write-Host "Well, there's always next year"
ElseIf ($newOrleansSaints)
  Write-Host "Lifelong Saints fans never give up"
  WriteHost "Not a football fan, I guess!"

The if statement is useful in many circumstances, but it's not the best way to perform tests where you want to evaluate one value against several possible choices. In that case, you use the switch statement, which lets you specify a set of possible values and a block of code to be executed for each. As an example, you could construct a switch statement to evaluate the odds of a particular NFL team going to the Super Bowl:

Switch ($teamName)
  "Saints" \{$odds = 0.5; WriteHost "Maybe...";\}
  "Lions" \{ $odds = 0.0; WriteHost "No way!";\}
  "Giants" \{$odds = 0.9; WriteHost "Looks likely at this point.";\}

Note that in this example, the code executed for each case of the switch statement contains more than one statement, separated by semicolons and enclosed by curly braces. Braces are an important piece of EMS syntax. You can enclose multiple statements in braces to have them treated as a group in loops and conditional statements.

You might also have noticed that the switch statement above uses string comparisons. EMS also lets you use range comparisons, regular expressions, and wildcards as part of the individual switch statements. For example, you can write the following switch statement to convert from a numerical grade to a letter grade:

Switch ($testScore)
  \{ $_ -lt 60\} \{$letterGrade = "F"; break;\}
  \{$_-lt 70\} \{$letterGrade = "D"; break; \}
  \{$_-lt 80\} \{$letterGrade = "C"; break; \}
  \{$_-lt 90\} \{$letterGrade = "B"; break; \}
  \{$_-lt 100\} \{$letterGrade = "A"; break; \}

Note that each comparison in the switch statement uses the $_ token to represent the current object, along with a relational operator.

Loops and Iterations
Frequently, you'll need to repeat a single action more than once to get something done. This ability is intrinsic to the way PowerShell pipelining works. For example, let's say you get a group of mailboxes and change an attribute by using Get-Mailbox and Set-Mailbox. Get-Mailbox returns a set of objects, each one of which is passed to Set-Mailbox—you don't have to write your own loop to make that happen.

You can iterate over a block of code in several ways. First, you can use a for statement. The for statement continues executing as long as the condition specified in the statement is true. You need to specify an initializer for the loop, a condition to evaluate, and an action to take for each iteration. This sounds more confusing than it is. Take a look at this example:

For ( $i = 0; $i -lt 15; $i=$i+1)
  Write-Host "The value of i is now " $i;

In this example, which prints out one line for each integer from 1 to 15, $i=0 is the initializer. We call $i the loop index, and the initializer prepares it for use in the loop. The condition we test is whether $i is less than 15; for each pass through the loop, the value of $i is incremented by 1. The actual code executed in the loop is a simple Write-Host statement, but you could of course take more complicated actions.

The second method for creating a loop is the foreach statement. This statement doesn't need an initializer or a condition to evaluate because it's designed to be used with collections of objects, such as arrays or results returned from another cmdlet. Use foreach when you want to take some action for each object or item in a set, regardless of the number. Here's an example from Microsoft's documentation:

Foreach ($file in Get-ChildItem)
  Write-Host $file;

This code gets all the files in the current directory (which is what Get-ChildItem does) and iterates over the collection of files, printing the name of each one. Foreach is valuable because it frees you from having to worry about how many items your loop needs to handle. Of course, the code executed in the foreach statement can test the objects to make decisions about whether to process the item or not.

What if you want to continue executing a block of code until a condition changes? In that case, you'd probably use the while statement. Here's an example:

$keepGoing = TRUE;
while ($keepGoing)
  Write-Host "This loop will never stop.";

In this case, of course, the loop never stops because the value of $keepGoing never changes; in a real script, you'd need to update the value of $keepGoing within the loop so that the script terminates when it should. There's a variant of the while statement, the Do While statement, that checks the condition at the end of the loop, so statements in the loop block are always executed at least once.

A Simple Example Script
Let's put all these pieces together—along with some additional ones—to examine a script that I built for a recent project. The script, batch-spam.ps1, is scheduled to run every two hours; it examines a specified directory, gathers up a set of files (in this case, spam email messages), compresses them into a .zip file, moves the .zip file to a directory, then exits. Listing 1 shows the code for this script.

I use a companion script, not shown here, that runs on another machine to grab the compressed spam files, decompress them, and analyze them, which is a more complicated process. In the rest of this article, I'll explain how batch-spam.ps1 works. Note that I won't be talking about what individual PowerShell cmdlets do, but you can find more information in the Learning Path on the right side of the article page or check Microsoft's documentation for anything that's unfamiliar.

As you can see at callout A in Listing 1, the script starts by defining a PowerShell function. A function is a block of code that returns a value; that's all. In this case, the function takes an argument (a date and time specifier) and returns true if that date represents today and false otherwise. I borrowed this function from PowerShell architect Jeffrey Snover's blog, which is a fertile source for PowerShell information.

Next, at callout B, you'll see a short block of code that assigns variables for the directories I want the script to use for collecting spam and dropping off the compressed folders. The code in callout C gets the current date and time, formats it with the month number and day first, adding leading zeros to single-digit numbers, then turns the date into a string that will be the name of a subfolder of the target directory. This structure lets the script batch spam from multiple days into a single target folder but keeps each day in its own subfolder.

After we have the date and time, the script can check to see whether that folder already exists. If it does, it will be emptied; if it doesn't exist, it will be created. To make this decision, the script uses a simple if statement. The \[void\] typecast is required to ignore the result we get back from creating the directory.

Now we can move spam messages from the spam collection folder to the appropriate folder for the date they were received. As the code at callout D shows, the script uses Get-ChildItem to get all the files in the source directory, then pipes the result to Copy-Item to actually copy the files.

The next part, compressing the target folder, is a bit tricky. PowerShell doesn't include built-in functions for manipulating .zip files. However, you can call COM methods from within PowerShell scripts, which is demonstrated in the code at callout E. To create a .zip file, we need to create a file and set the first 22 bytes to a specific string that indicates that the file is a .zip file. You do this through the magic of the Set-Content cmdlet. After the file's header has been set, we can create a new interface to the Windows Explorer shell and use it to add files to our target .zip file. I adapted this function from Mike Hodnick's blog.

Finally, we move the newly created zip file to the target area, which callout F shows. Notice that in this case, I used Move-Item because I wanted to move the file, instead of using Copy-Item as when moving the spam files to the target.

Go Forth and Script!
The best way to get experience with writing EMS scripts is to try your hand at writing a few. Thanks to the -WhatIf and -Confirm flags, you can do so without fear that you'll damage anything important. There are lots of useful examples of PowerShell scripts on the Internet as well. In particular, Microsoft has numerous example scripts at The Script Center Script Repository that will prove quite useful as you explore what you can achieve with EMS. If you’re interested in more information on Exchange management with EMS, let me know what topics you’d like to see covered.

Listing 1: batch-spam.ps1
# ******* BEGIN CALLOUT A *******
# nifty date function from Jeffrey Snover's blog
function isToday (\[datetime\]$date) 
\{\[datetime\]::Now.Date  -eq  $date.Date\}
# ******* END CALLOUT A *******

# ******* BEGIN CALLOUT B *******
# set everything up
$sourceDir = "c:\temp\"
$targetDir = "c:\temp\outgoing"
$dropDir = $targetDir 
# ******* END CALLOUT B *******

# ******* BEGIN CALLOUT C *******
# get the current date and time
$targetFolder = \[string\]($targetDir + "\" + (Get-Date -format "MMdd-HHmm"))

# if it doesn't already exist, create a target folder for this date/time
# if it does exist, empty it out, but don't remove it
if (Test-Path $targetFolder)
  Write-Host "Emptying target folder $targetFolder"
  Get-ChildItem $targetFolder | Remove-Item
  \[void\] (mkdir $targetFolder)
  Write-Host "Created $targetFolder"
# ******* END CALLOUT C *******

# ******* BEGIN CALLOUT D *******
# move any extant spam from today to the target folder.
$sourceSpec = $sourceDir + "*.eml"
Write-Host "Moving source files from $sourceSpec to $targetFolder..."
Get-ChildItem $sourceSpec | Copy-Item -destination $targetFolder
# ******* END CALLOUT D *******

# ******* BEGIN CALLOUT E *******
# compress the target folder
$zipTarget = $targetFolder + ".zip"
Write-Host "Compressing target files in $targetFolder"

# zip functions adapted from Mike Hodnick's blog
  $fileList = Get-ChildItem $targetFolder
  Write-Host "   Creating zipfile $zipTarget"
  if (-not (test-path $zipTarget)) 
    set-content $zipTarget ("PK" + \[char\]5 + \[char\]6 + ("$(\[char\]0)" * 18)) 
  $ZipFile = (new-object -com shell.application).NameSpace($zipTarget) 
  $numFiles = $fileList.count
  if ($numFiles -gt 0) 
    Write-Host "    Adding $numFiles files to zip"
    $fileList | foreach \{$zipfile.CopyHere($_.fullname)\} 
  \} else
    Write-Host "    File list was empty"
# ******* END CALLOUT E *******

# ******* BEGIN CALLOUT F *******
# move the zip file to the drop area
Write-Host "Moving file to target"
Move-Item -destination $dropDir $zipTarget 

# done
Write-Host "Done!"
# ******* END CALLOUT F *******
Hide comments


  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.