RSS Subscription 167 Posts and 2,643 Comments

Another Exchange Scriplet (Moving Mailboxes)

This one I created for the following reasons:
1. Mailbox Database was filling up
2. Wanted to take the filled up database and split them across two different databases.

The script will sort all the mailboxes by size and take 40% of the largest mailboxes and move them to a specified database and take the remaining 60% and move them to the other database.

If you don’t want to specify the exact database name and just want PowerShell to use part of the name you specify to find a matching database, you can change the part where it states {$_.Name -eq $sourceDB} to have -like instead of -eq. Do the same for the following two pieces of code: {$_.Name -eq $targetDBLarge} and {$_.Name -eq $targetDBSmall}. -eq needs the exact match for the database name and -like basically takes the database name and puts wild cards around it so it’s more like *databaseyouspecify* and then finds a matching database that is like the name you specified.

Important: I added -whatif switches to the move-mailbox commands below to be extra safe. To actually move the mailboxes, you’ll need to remove the -whatif.  And as always, MAKE SURE YOU TEST THIS IN LAB BEFORE RUNNING IT IN PRODUCTION AND HAVE A GOOD BACKUP UP AD/EXCHANGE/ETC AS WELL!

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$sourceDB = "Specify your Source Database Here"
$targetDBLarge = "Specify your larger database in which 40% of the largest mailboxes will be moved to"
$targetDBSmall = "Specify your smaller database in which the remaining 60% of the smaller mailboxes will be moved to"
 
############################################
####### Don't modify below this line #######
############################################
 
$a = Get-MailboxStatistics -Database $(Get-Mailboxdatabase | Where-Object {$_.Name -eq $sourceDB}) | where {$_.ObjectClass -NotMatch '(SystemAttendantMailbox|ExOleDbSystemMailbox)'} | Sort-Object Totalitemsize
 
$rounded = [math]::round($a.count * .4)
$first = $rounded
$last = $a.count - $first
 
$a | Select-object -First $first | Move-Mailbox -TargetDatabase $(Get-Mailboxdatabase | Where-Object {$_.Name -eq $targetDBLarge}) -whatif
$a | Select-object -Last $last | Move-Mailbox -TargetDatabase $(Get-Mailboxdatabase | Where-Object {$_.Name -eq $targetDBSmall}) -whatif
Share

A few Exchange PowerShell scriptlets

Figured I would share a few scriptlets I’ve written recently.  Hopefully some of you may find them useful at some point at least.

Scriptlet 1

Find out all users who live on a specific mailbox without having to type the entire mailbox database name:

?View Code POWERSHELL
1
get-mailbox -database $(get-mailboxdatabase | where-object {$_.name -like "*Database1*"}) | Format-Table Name,Database -wrap -autosize

Scriptlet 2

Get statistics on a specific user without knowing the entire name or if you’d want to find out statistics on all users who have the first name John or last name Doe:

?View Code POWERSHELL
1
Get-MailboxStatistics -server serverhere | Where-Object {$_.DisplayName -like "*MailboxUserName*"} | Format-Table DisplayName,ItemCount,TotalItemSize

Scriptlet 3

When you are in an environment with multiple Mailbox Servers, it may be annoying to find what disconnected mailbox lives on which server since the Exchange Management Console will only connect to one server at a time and when connected, it’ll only show disconnected mailboxes for that specific server.  This scriptlet will cycle through all mailbox servers and report back all disconnected mailbox servers and which disconnected mailbox lives on which mailbox server.

?View Code POWERSHELL
1
2
3
4
$mbx = Get-ExchangeServer | Where-Object {$_.IsMailboxserver -eq $true}
foreach ($server in $mbx) {
	Get-Mailboxstatistics -Server $Server | Where-Object { $_.DisconnectDate -ne $null } | Format-Table DisplayName,ItemCount,OriginatingServer -wrap
}

Scriptlet 4

Based on a CSV file, export users contacts that were created after a certain start date to a PST file.  A copy of how the CSV should be formatted is located here.  While the CSV contains multiple columns, the script only utilizes the Account Name column by default.  If you want to modify what columns it uses, you can change the line below that starts with $AdName =.  After the script pulls the name in the Account Name field, it finds the mailbox using the Get-Mailbox command with the name that was specified in the Account Name field.

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$file = "pstusers.csv"
$PSTLocation = "C:\PSTs"
$StartDate = "03/20/2009"
 
############################################
####### Don't modify below this line #######
############################################
$erroractionpreference = "SilentlyContinue"
if (!(Test-Path -path $PSTLocation)) {
	Write-Host "Creating the Directory $PSTLocation" -ForegroundColor Yellow
	New-Item -ItemType "Directory" -Path $PSTlocation
}
if (Test-Path $file) {
	$excel = Import-CSV $file
	foreach ($line in $excel) {
		$AdName = $line."Account Name"
		$Mailbox = Get-Mailbox $AdName
		if ($Mailbox) {
			Write-Host "$AdName is being exported to $PSTLocation"
			$Mailbox | Export-Mailbox -PSTFolderPath $PSTLocation -StartDate $StartDate -ExcludeFolders "\Inbox","\Deleted Items","\Drafts","\Junk E-mail","\Outbox","\Sent Items","\Journal","\Calendar","\Notes","\Tasks"
		}
		else { Write-Warning "$ADName Mailbox Does not Exist" }
	}
}
else {
	Write-Warning "The file $file does not exist"
}

Scriptlet 5

If you are familiar with Quest Notes Migrator for Exchange, you may have had the chance to migrate data to a PST file.  What you will see, is that the PST gets migrated with the Display Name of the user.  For example, we if migrated the Notes mailbox of Elan Shudnow to a PST, the PST file name would be Elan Shudnow.PST. A lot of people won’t want to migrate the PST directly from Quest NME to a user’s home share because that user’s home share may be located in a remote site and opening a PST file across a network isn’t really a good idea; especially over long distances.

So I wrote a script so you can migrate all the PST files locally and then it’ll check the Display Name portion of the PST file and use that display name to find a matching user in AD and then find their home directory and then copy that PST file to that home share.

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Set Location to where the PST files are started
Set-Location "E:\"
 
$pstData = Get-ChildItem -recurse | Where-Object {$_.Name -like "*.pst"}
 
function Get-HomeDirectory ($displayName) {
	$ads = New-Object System.DirectoryServices.DirectorySearcher([ADSI]'')
	$ads.filter = "(&(objectClass=Person)(displayName=$displayName))"
	$s = $ads.FindOne()
	return $s.GetDirectoryEntry().homeDirectory
}
 
foreach ($pstUser in $pstData) {
	$PSTName = $pstUser.Name
	$PSTDisplayName = $pstUser.Name.split(".")[0]
	$PSTDisplayName = $PSTDisplayName.split("-")[0]
	$Path = $PSTUser.DirectoryName + "\" + $PSTUser.Name
	$Directory = Get-HomeDirectory $PSTDisplayName
	Copy-Item $Path $Directory
}
Share

PowerShell and its future

As has been stated in the past, I am currently learning PowerShell (I am new to scripting).  Unfortunately, the last month has been a bit busy for me and I haven’t been able to focus on PowerShell at all.  But I’m getting back into it now.  Upon getting back into it, I discovered a couple cool utilities as well as information about PowerShell’s future and figured I would share this information with you.

I was debating on purchasing AdminScriptEditor for its ability to create create PowerShell forms and because I’d like to have a good Form Builder.  Recently, Sapien Technologies, Inc. released a free tool called PrimalForms to do exactly this, create PowerShell Windows Forms.   They also have many other free tools such as WMIExplorer, Windows PowerShell Help Tool, and many more.  It’s only a matter of time before the next version of PrimalScript is out that’ll have some awesome features for PowerShell.

I also found some more information about PowerShell’s future and how it will relate to Windows Server 2008 R2 and Windows 7.  Fortunately, Windows PowerShell will be installed and accessible by default in both.  This version of Windows PowerShell will be version 2 which will support many new features such as a graphical powershell window, the ability to do remoting, and much more.  You can read about what’s coming in version 2 here.  Information about PowerShell being installed by default on Windows Server 2008 R2 and Windows 7 can be found here.

As many PowerShell people know, there was a lack of AD cmdlets in Server 2008.  The reason for this is Server 2008 development was too far along for cmdlets.  Fortunately, Quest provides QAD cmdlets to provide a lot of Active Directory Manamgent to alleviate the tediousness of connecting to ADSI to perform actions.  These QAD cmdlets can be located here.

So what will will Server 2008 R2 and Windows 7 bring that makes having PowerShell version 2 so wonderful?  Well, having Powershell version 2 is only one thing.  There’s also a net total so far of 574 cmdlets added!  WOW!  This isn’t even including Providers.  The AD Management tool(s) are built on top of PowerShell and so are the remoting capabilities of Server Manager.  Dmitry posted a list of cmdlets here.

Microsoft is definitely moving towards a model that anything that can be done in the GUI can be done through the CLI.  And  PowerShell is a great scripting language which means you can easily use tools such as PrimalForms to create your own Windows GUI to run PowerShell cmdlets and display them into a GUI as you see fit.  You can read more about this here.

Share

Obtaining Exchange Information including Edge with PowerShell

So the last month I’ve been learning how to do PowerShell scripting. I’m new at scripting and figured I would share my first Exchange script due to sheer excitement. I’d like to give a special thanks to Shay Levy @ http://blogs.microsoft.co.il/blogs/ScriptFanatic. Shay has been a tremendous help over @ http://powershellcommunity.org when I run into issues that any typical novice runs into (especially with scripting). The other folks have been a great help as well.

I started this script last week and will be adding to it as I gain more knowledge. The skeleton of the script is complete with 1 function so far (to get disk information) and will allow me to easily add more functions to the script. I wanted to provide a method to dynamically check if a server was an Edge Server and prompt for authentication to allow PowerShell to obtain information from my Edge Servers.

With an Edge server, it is not going to be a part of your corporate domain . It may be a part of a forest/domain dedicated in your DMZ, but not your corporate forest/domain. Typically if you want to pull information from a domain-joined machine that is a part of your corporate domain, you just run a command against it and it’ll work if you have the access to do so. But since the Edge server is a part of a workgroup or DMZ forest/domain, it’s not that easy. This script will check if you are trying to check an Edge Server, and if so, it will display on the console that it is checking for Edge authentication and prompt you for credentials for that Edge Server.

Here’s the script I have so far:

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# Define what paramters (-role) can be utilized when running the switch
Param(
[string] $role = "All"
)
 
# Define what values (Mbx, Cas, Etc.) can be utilized when using our -role parameter
switch ($role)
{
	Mbx { $role = "IsMailboxServer" }
	Cas { $role = "IsClientAccessServer" }
	Um { $role = "IsUnifiedMessagingServer" }
	Hub { $role = "IsHubTransportServer" }
	Edge { $role = "IsEdgeServer" }
}
 
# Test host to see if it replies to ping prior to allowing information gather functions to proceed.
function Ping-Host
{
       $result = Gwmi -Query "SELECT * FROM Win32_PingStatus WHERE Address='$server'"
       if ($result.statuscode -eq 0) {$true} else {$false}
}
 
# Function to display disk information.  If the server being checked is an Edge Server, you will be prompted for authentication.
function Get-DiskInformation
{
	""
	"Server: $server"
	if ($server.isEdgeServer)
	{
                $erroractionpreference = "SilentlyContinue"
		$cred = Get-Credential
		if ($cred) { $disk = Get-WmiObject Win32_LogicalDisk -namespace root\cimv2 –filter "DriveType = 3" -ComputerName $server -Credential $cred }
		else { Write-Warning "Could not obtain disk information for $server due to no credentials being provided" ; break }
                $erroractionpreference = "Continue"
	}
	else { $disk = Get-WmiObject Win32_LogicalDisk -namespace root\cimv2 –filter "DriveType = 3" -ComputerName $server }
 
	foreach($d in $disk)
	{
		$obj = New-Object PSObject
		$obj | Add-Member NoteProperty "Size(G)" ([math]::round($d.Size/1GB,2))
		$obj | Add-Member NoteProperty "Freespace(G)" ([math]::round($d.FreeSpace/1GB,2))
		$obj | Add-Member NoteProperty "Used(G)" ([math]::round($d.Size/1GB - $d.FreeSpace/1GB,2) )
		$obj | Add-Member NoteProperty "Freespace(%)" ([Math]::Round((($d.FreeSpace/1GB) / ($d.Size/1GB) * 100),2))
		$obj | Add-Member NoteProperty "Usedspace(%)" ([Math]::Round(((1 - ($d.FreeSpace/1GB) / ($d.Size/1GB)) * 100),2))
		$obj
	}
}
 
# Place servers into the $colServers variable which will later be fed into Functions
if ($role -ne "All" -and $role -ne "IsEdgeServer") { $colServers = Get-ExchangeServer | Where-Object {$_.$role -eq "True" -and $_.IsEdgeServer -ne "True"} }
elseif ($role -ne "All" -and $role -eq "IsEdgeServer") { $colServers = Get-ExchangeServer | Where-Object {$_."IsEdgeServer" -eq "True"} }
else { $colServers = Get-ExchangeServer }
 
if (!$colServers) { Write-Warning "There are no servers of the specified type to gather information for." }
else
{
	foreach ($server in $colServers)
	{
		if (Ping-Host -eq "$true")
		{
			Get-DiskInformation
		}
		else
		{
			Write-Warning "$server is not pingable"
		}
	}
}

There’s one piece of the script I want to disuss. It’s the function for Get-DiskInformation. I used to have the function written as such.

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
function Get-DiskInformation
{
	""
	"Server: $server"
	$(if ($server.isEdgeServer) { Get-WmiObject Win32_LogicalDisk -namespace root\cimv2 –filter "DriveType = 3" -ComputerName $server -Credential (Get-Credential) }
	else { Get-WmiObject Win32_LogicalDisk -namespace root\cimv2 –filter "DriveType = 3" -ComputerName $server }) | `
	Format-Table -autosize DeviceID,VolumeName,`
	@{Label="Size(G)";Expression={[math]::round($_.Size/1GB,2)}},`
	@{Label="Freespace(G)";Expression={[math]::round($_.FreeSpace/1GB,2)}},`
	@{Label="Used(G)";Expression={[math]::round($_.Size/1GB - $_.FreeSpace/1GB,2)}},`
	@{Label="Freespace(%)";Expression={[Math]::Round((($_.FreeSpace/1GB) / ($_.Size/1GB) * 100),2)}},`
	@{Label="Usedspace(%)";Expression={[Math]::Round(((1 - ($_.FreeSpace/1GB) / ($_.Size/1GB)) * 100),2)}}
}

Now the issue with how I used to have it written is that it forces Format-Table and a specific style. So essentially, I am forcing the output to look a certain way. There’s no way for the user to dynamically change the way the output appears and will mess up the formatting if you wanted to place the data into an Excel sheet for example.

Now let’s take a look at the way I have it in the final script I posted.

?View Code POWERSHELL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Get-DiskInformation
{
	""
	"Server: $server"
	if ($server.isEdgeServer)
	{
                $erroractionpreference = "SilentlyContinue"
		$cred = Get-Credential
		if ($cred) { $disk = Get-WmiObject Win32_LogicalDisk -namespace root\cimv2 –filter "DriveType = 3" -ComputerName $server -Credential $cred }
		else { Write-Warning "Could not obtain disk information for $server due to no credentials being provided" ; break }
                $erroractionpreference = "Continue"
	}
	else { $disk = Get-WmiObject Win32_LogicalDisk -namespace root\cimv2 –filter "DriveType = 3" -ComputerName $server }
 
	foreach($d in $disk)
	{
		$obj = New-Object PSObject
		$obj | Add-Member NoteProperty "Size(G)" ([math]::round($d.Size/1GB,2))
		$obj | Add-Member NoteProperty "Freespace(G)" ([math]::round($d.FreeSpace/1GB,2))
		$obj | Add-Member NoteProperty "Used(G)" ([math]::round($d.Size/1GB - $d.FreeSpace/1GB,2) )
		$obj | Add-Member NoteProperty "Freespace(%)" ([Math]::Round((($d.FreeSpace/1GB) / ($d.Size/1GB) * 100),2))
		$obj | Add-Member NoteProperty "Usedspace(%)" ([Math]::Round(((1 - ($d.FreeSpace/1GB) / ($d.Size/1GB)) * 100),2))
		$obj
	}
}

As you can see, it looks quite different. What I did here was create a PowerShell object. This allows me to add data to this object which in return allows me to pipe our command into many different outputs such as Format-Table, Format-List (which will be default since there are >=4 lines of data being returned), Excel, etc.. I’ve also added other error checking such as if you don’t enter a user/password for your Edge Server, it won’t throw a bunch of errors on the screen. Instead, it will provide a nice warning message letting you know credentials were not provided.

Let’s take a look at an example at pulling only disk information for a Mailbox (and yes this is a testlab and Exchange is running on my DC which I would never do in production!).

So what happens if we didn’t specify the -role command or specified All for the role command? The same thing will happen since -role switch is automatically assumed and the default for the command is All.

Since we selected All, it found an Edge Server in the environment but the script runs a ping test on all servers before trying to obtain information. Since our Edge Server was down, we were notified of this. Now after bringing my Edge Server online, let’s run a -role All again.

We still pulled information for our domain-joined Exchange Server, but now that our Edge server is up and we can ping it, it prompts us for authentication. On the screen, the server that it is prompting for is OCS-EXCEdge which is the name of our Edge Server. So let’s enter our credentials for this OCS-EXCEdge server.

We now authenticated with the Edge Server and obtained disk information. If we instead hit cancel on the authentication prompt, we would get:

And the last thing I want to show is another safe guard in case you enter a wrong server type.

Share