<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>sidefumbling</title>
	<atom:link href="http://www.flobee.net/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.flobee.net</link>
	<description>The consequence of not having six hydrocoptic marzelvanes.</description>
	<lastBuildDate>Fri, 18 May 2012 17:56:08 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
		<item>
		<title>Date/Time Localization in Exchange Cmdlets</title>
		<link>http://www.flobee.net/date-time-localization-in-exchange-cmdlets/</link>
		<comments>http://www.flobee.net/date-time-localization-in-exchange-cmdlets/#comments</comments>
		<pubDate>Fri, 18 May 2012 17:56:08 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=664</guid>
		<description><![CDATA[Exchange is nothing if not inconsistent with how date/time values are interpreted when used as an input parameter or when displayed as a property. I tested some cmdlets I commonly use and have documented how Exchange handles the input and output of the date/time values. Get-MessageTrackingLog Input: Format is with respect to the time zone [...]]]></description>
			<content:encoded><![CDATA[<p>Exchange is nothing if not inconsistent with how date/time values are interpreted when used as an input parameter or when displayed as a property. I tested some cmdlets I commonly use and have documented how Exchange handles the input and output of the date/time values.</p>
<table border="1" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td valign="top">Get-MessageTrackingLog</td>
<td valign="top"><strong>Input</strong>: Format is with respect to the time zone of the server to which your shell is connected.</p>
<p><span style="text-decoration: underline;">Scenario</span>: User in Pacific Time (PT) sends a message at 8:23 AM. If you are connected to a server in the same time zone, the input would be 8:23 AM.  If you are connected to a server in Mountain Time (MT), the input is 9:23 AM. Implicit remoting uses the server’s time, not the local workstation.</p>
<p>&nbsp;</p>
<p><strong>Output</strong>: Format is localized to the time zone of the shell running the cmdlet.</p>
<p><span style="text-decoration: underline;">Scenario</span>: Shell running on a server in MT will return the time adjusted for MT.  Implicit remoting uses the time of the machine running the shell, which means a system in PT with implicit remoting to a server in MT will return the time in PT.</td>
</tr>
<tr>
<td valign="top">Get-ActiveSyncDeviceStatistics</td>
<td valign="top"><strong>Output</strong>: Format is displayed in UTC, regardless of time zone of the server connected to or the system running the shell.</td>
</tr>
<tr>
<td valign="top">Get-Queue</td>
<td valign="top"><strong>Output</strong>: Format is localized to the time zone of the shell running the cmdlet.</p>
<p><span style="text-decoration: underline;">Scenario</span>: Shell running on a server in MT will return the time adjusted for MT.  Implicit remoting uses the time of the machine running the shell, which means a system in PT with implicit remoting to a server in MT will return the time in PT.</td>
</tr>
<tr>
<td valign="top">Set-MailboxAutoReplyConfiguration</td>
<td valign="top"><strong>Input</strong>: Format is with respect to the time zone of the server to which your shell is connected.</p>
<p><span style="text-decoration: underline;">Scenario</span>: User in PT wants OOF to start at 9:00 AM. If you are connected to a server in the same time zone, the input would be 9:00 AM.  If you are connected to a server in MT, the input is 10:00 AM. Implicit remoting uses the server’s time, not the local workstation.</td>
</tr>
<tr>
<td valign="top">Get-MailboxAutoReplyConfiguration</td>
<td valign="top"><strong>Output</strong>: Format is localized to the time zone of the shell running the cmdlet.</p>
<p><span style="text-decoration: underline;">Scenario</span>: Shell running on a server in MT will return the time adjusted for MT.  Implicit remoting uses the time of the machine running the shell, which means a system in PT with implicit remoting to a server in MT will return the time in PT.</td>
</tr>
<tr>
<td valign="top">Set-MailboxCalendarConfiguration</td>
<td valign="top"><strong>Input</strong>: Format is a “literal” time that will always be interpreted to be local to the value of the WorkingHoursTimeZone property, regardless of the time zone of system running the shell.</p>
<p><span style="text-decoration: underline;">Scenario</span>: A user’s working hours start time needs to be changed to 9:00 AM local time. The input will be 9:00 AM, regardless of the user’s time zone, the time zone of the server connected to, or the system running the shell.</td>
</tr>
<tr>
<td valign="top">Get-MailboxCalendarConfiguration</td>
<td valign="top"><strong>Output</strong>: Format is non-localized, always interpreted to be local to the WorkingHoursTimeZone property.</p>
<p><span style="text-decoration: underline;">Scenario</span>: A user’s working hours start time is 8:00 AM local time. The time will always be displayed as 8:00 AM, regardless of the time zone of the system running the shell.</td>
</tr>
<tr>
<td valign="top">Search-AdminAuditLog</td>
<td valign="top"><strong>Input</strong>: Format is with respect to the time zone of the server to which your shell is connected.</p>
<p><span style="text-decoration: underline;">Scenario</span>: A change is made at 8:23 AM PT. If you are connected to a server in the same time zone, the input would be 8:23 AM.  If you are connected to a server in Mountain Time (MT), the input is 9:23 AM. Implicit remoting uses the server’s time, not the local workstation.</p>
<p>&nbsp;</p>
<p><strong>Output</strong>: Format is localized to the time zone of the shell running the cmdlet.</p>
<p><span style="text-decoration: underline;">Scenario</span>: Shell running on a server in MT will return the time adjusted for MT.  Implicit remoting uses the time of the machine running the shell, which means a system in PT with implicit remoting to a server in MT will return the time in PT.</td>
</tr>
</tbody>
</table>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/date-time-localization-in-exchange-cmdlets/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Patch and reboot script for an entire Exchange environment, Part 4</title>
		<link>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-4/</link>
		<comments>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-4/#comments</comments>
		<pubDate>Fri, 20 Jan 2012 16:58:02 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=623</guid>
		<description><![CDATA[This post is part of a multi-part series: Part 1 Part 2 Part 3 Part 4 (this post) After enabling transcript logging, loading the Exchange snap-in, and connecting to SCOM, the server processing begins.  This is grouped by server role and production vs. DR. ProcessServerGroup $HTCAS 'HTCAS' ProcessServerGroup $UM 'UM' ProcessServerGroup $MB 'MB' BalanceDB ProcessServerGroup [...]]]></description>
			<content:encoded><![CDATA[<p>This post is part of a multi-part series:</p>
<p style="padding-left: 30px;"><a title="Patch and reboot script for an entire Exchange environment, Part 1" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-1/">Part 1</a><br />
<a title="Patch and reboot script for an entire Exchange environment, Part 2" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-2/">Part 2</a><br />
<a title="Patch and reboot script for an entire Exchange environment, Part 3" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-3/">Part 3</a><br />
Part 4 (this post)</p>
<p>After enabling transcript logging, loading the Exchange snap-in, and connecting to SCOM, the server processing begins.  This is grouped by server role and production vs. DR.</p>
<pre class="brush:ps;gutter:false">ProcessServerGroup $HTCAS 'HTCAS'
ProcessServerGroup $UM 'UM'
ProcessServerGroup $MB 'MB'
BalanceDB
ProcessServerGroup $DRHTCAS 'HTCAS'
ProcessServerGroup $DRUM 'UM'
ProcessServerGroup $DRMB 'MB'</pre>
<p>Based on my environment&#8217;s configuration where hub transport and client access server are colocated, the production HT/CAS servers are processed, then the UM servers, and then mailbox servers.  When the production mailbox servers are complete, the databases are re-balanced according to activation preference.  Then the DR servers are processed.</p>
<p>Let&#8217;s go through the <span id="codekeyword" class="flobeecode">ProcessServerGroup</span> function.</p>
<pre class="brush:ps">function ProcessServerGroup($serverGroup,$groupType)
	{
	foreach ($server in $serverGroup)
		{
		Write-Host "*******************Beginning processing of $server.*******************"
		$Host.UI.RawUI.WindowTitle = "Processing $server"
		if ($bSendStatusUpdates)
			{
			SendStatus "Processing beginning" $server
			}</pre>
<p>The function is called by passing the array that contains the server names, along with the role they serve. Looping through each server in the array, the display reflects the status. If status updates are enabled, line 9 calls a function with the status and the server name; here is the <span id="codekeyword" class="flobeecode">SendStatus</span> function:</p>
<pre class="brush:ps;gutter:false">function SendStatus($details,$server)
	{
	$mailSubject = "$server status update"
	$mailBody = "$server: $details"
	$smtp = New-Object Net.Mail.SmtpClient($mailServer)
	$smtp.Send($mailSender,$statusRecipients,$mailSubject,$mailBody)
	}</pre>
<p>The only noteworthy thing about the previous function is the use of the SmtpClient .NET class instead of the <span id="codekeyword" class="flobeecode">Send-MailMessage</span> cmdlet. This is because of the different encoding used between them. When sending to a mobile device via SMS, the cmdlet&#8217;s encoding causes non-printable characters to be displayed in the SMS body. Using the .NET class avoids this.</p>
<p>The next section of the <span id="codekeyword" class="flobeecode">ProcessServerGroup</span> function is for SCOM. If SCOM updates are enabled and the remote PowerShell session to the RMS is established, a function to start maintenance mode is called:</p>
<pre class="brush:ps;gutter:false">		if ($bUpdateSCOM -and $global:bSCOMSessionOpen)
			{
			StartSCOMMaintenance $server
			}</pre>
<p>And here is that function:</p>
<pre class="brush:ps">function StartSCOMMaintenance($nodeName)
	{
	WriteTime;Write-Host 'Putting server into maintenance mode in SCOM...'
	$ErrorActionPreference = 'Stop'
	try
		{
		Invoke-Command -ScriptBlock {param($nodeName) $serverCrit = "DisplayName like '$nodeName%'"} -ArgumentList $nodeName -Session $global:scomSession
		Invoke-Command -ScriptBlock {$monObj = Get-MonitoringObject -MonitoringClass $monClass -Criteria $serverCrit} -Session $global:scomSession
		Invoke-Command -ScriptBlock {param($duration) $now = Get-Date;$maintStartTime = $now.ToUniversalTime();$maintEndTime = $now.AddMinutes($duration).ToUniversalTime()} -ArgumentList $maintModeDuration -Session $global:scomSession
		Invoke-Command -ScriptBlock {New-MaintenanceWindow -MonitoringObject $monObj -StartTime $maintStartTime -EndTime $maintEndTime -Reason PlannedApplicationMaintenance -Comment 'Patch and reboot'}  -Session $global:scomSession
		WriteTime;Write-Host 'Server successfully put into maintenance mode in SCOM.'
		$script:bMaintModeActive = $true
		}
	catch
		{
		WriteTime;Write-Host 'Warning: Server could not be put into maintenance mode in SCOM.' -ForegroundColor Yellow
		$script:bMaintModeActive = $false
		}
	$ErrorActionPreference = 'Continue'
	}</pre>
<p>Lines 7-10 send commands to the remote session. It took some time to figure out how to do this because it isn&#8217;t well documented. The gist is that the SCOM object for a server has to be found, then a new maintenance window created. You can&#8217;t have an open-ended window, so the duration is set to the variable defined earlier. If the server is successfully put into maintenance mode, Line 12 sets a variable with this state. If any of the previous steps fail, a warning is output to the screen. I don&#8217;t treat this as a fatal error, so the patching will continue.</p>
<p>The next post in the series will continue with the <span id="codekeyword" class="flobeecode">ProcessServerGroup</span> function and moving active database copies if a server with the mailbox role is being processed.</p>
<p>Download the complete script:Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-4/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Patch and reboot script for an entire Exchange environment, Part 3</title>
		<link>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-3/</link>
		<comments>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-3/#comments</comments>
		<pubDate>Thu, 12 Jan 2012 19:08:26 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=614</guid>
		<description><![CDATA[This post is part of a multi-part series: Part 1 Part 2 Part 3 (this post) Part 4 The first part of the script body gets the current window title, then checks if transcript is running, starting it if not. #Region Script Body $savedTitle = $Host.UI.RawUI.WindowTitle if (!($global:transcriptIsRunning)) { Start-Transcript -Path $transcriptLog -Append $global:transcriptIsRunning = [...]]]></description>
			<content:encoded><![CDATA[<p>This post is part of a multi-part series:</p>
<p style="padding-left: 30px;"><a title="Patch and reboot script for an entire Exchange environment, Part 1" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-1/">Part 1</a><br />
<a title="Patch and reboot script for an entire Exchange environment, Part 2" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-2/">Part 2</a><br />
Part 3 (this post)<br />
<a title="Patch and reboot script for an entire Exchange environment, Part 4" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-4/">Part 4</a></p>
<p>The first part of the script body gets the current window title, then checks if transcript is running, starting it if not.</p>
<pre class="brush:ps;gutter:false">#Region Script Body
$savedTitle = $Host.UI.RawUI.WindowTitle
if (!($global:transcriptIsRunning))
	{
	Start-Transcript -Path $transcriptLog -Append
	$global:transcriptIsRunning = $true
	}</pre>
<p>Since the script can&#8217;t use remote PowerShell to connect to an Exchange server that will be rebooted, the snap-in is loaded locally.</p>
<pre class="brush:ps;gutter:false">#Load Exchange 2010 snap-in
if (-not (Get-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 -ErrorAction SilentlyContinue))
	{Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010}</pre>
<p>If the SCOM variable is set to true, then the remote PowerShell connection to the RMS is loaded.</p>
<pre class="brush:ps;gutter:false">if ($bUpdateSCOM)
	{
	WriteTime;Write-Host 'Option: SCOM maintenance mode support enabled.'
	LoadSCOMShell
	}</pre>
<p>Throughout the script you will see that whenever something is output to the console it is prefixed with the time. This is accomplished with a function to format it the way I want to see it.</p>
<pre class="brush:ps;gutter:false">function WriteTime
	{
	$a = Get-Date -Format g
	Write-Host "$a`: " -NoNewline
	}</pre>
<p>This is the function to connect to the SCOM RMS:</p>
<pre class="brush:ps;gutter:false">function LoadSCOMShell
	{
	WriteTime;Write-Host 'Creating remote session to SCOM server...'
	$ErrorActionPreference = 'Stop'
	try
		{
		$global:scomSession = New-PSSession -ComputerName $rmsName
		Invoke-Command -ScriptBlock {Add-PSSnapin 'Microsoft.EnterpriseManagement.OperationsManager.Client'} -Session $global:scomSession
		Invoke-Command -ScriptBlock {Set-Location 'OperationsManagerMonitoring::'} -Session $global:scomSession
		Invoke-Command -ScriptBlock {param($rmsName) New-ManagementGroupConnection -connectionString:$rmsName | Out-Null} -Session $global:scomSession -ArgumentList $rmsName
		Invoke-Command -ScriptBlock {param($rmsName) Set-Location $rmsName} -Session $global:scomSession -ArgumentList $rmsName
		Invoke-Command -ScriptBlock {$monClass = Get-MonitoringClass | Where-Object {$_.DisplayName -eq 'Windows Server'}} -Session $global:scomSession
		WriteTime;Write-Host "Successfully created remote session to SCOM server." -ForegroundColor Green
		$global:bSCOMSessionOpen = $true
		}
	catch
		{
		WriteTime;Write-Host 'Error: Unable to create remote session to SCOM server...' -ForegroundColor Red
		$global:bSCOMSessionOpen = $false
		}
	$ErrorActionPreference = 'Continue'
	}</pre>
<p>The function creates a new session to the RMS, scoping the variable as <span id="codevariable" class="flobeecode">$global</span> so that if the script aborts and you restart it in the same shell, the session can still be used instead of creating a new one. Once the session is established, several script blocks are executed in it to load the SCOM snap-in and configure it so that a server can be put into maintenance mode. It took some time to figure out how to do all this because the PowerShell documentation for SCOM is very poor and code samples on other sites were for working with clusters or doing other tasks. If no error occurs during any of this, a variable is set to indicate that the session is working. If an error does occur, the script does not abort. I only treat this as a warning since an inability to put a server into maintenance shouldn&#8217;t keep the server from being patched. The status variable is set so that the script won&#8217;t try and put servers into maintenance mode if doing so will only result in an error anyway.</p>
<p>The next post in the series will get into the actual processing of the servers.<br />
Download the complete script:Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-3/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Patch and reboot script for an entire Exchange environment, Part 2</title>
		<link>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-2/</link>
		<comments>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-2/#comments</comments>
		<pubDate>Fri, 30 Dec 2011 17:59:44 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=604</guid>
		<description><![CDATA[This post is part of a multi-part series: Part 1 Part 2 (this post) Part 3 The first section, or region as used in PowerGUI, is where all the variables are defined.  The script will process servers based on their Exchange roles and whether they are in the production or standby (DR) data center.  For [...]]]></description>
			<content:encoded><![CDATA[<p>This post is part of a multi-part series:</p>
<p style="padding-left: 30px;"><a title="Patch and reboot script for an entire Exchange environment, Part 1" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-1/">Part 1</a><br />
Part 2 (this post)<br />
<a title="Patch and reboot script for an entire Exchange environment, Part 3" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-3/">Part 3</a></p>
<p>The first section, or region as used in <a title="PowerGUI" href="http://www.powergui.org" target="_blank">PowerGUI</a>, is where all the variables are defined.  The script will process servers based on their Exchange roles and whether they are in the production or standby (DR) data center.  For my environment, this means hub transport and client access are on the same server, while the mailbox and unified messaging roles are dedicated.</p>
<pre class="brush: ps">#Server groupings
[array]$HTCAS = 'prodhc01','prodhc02' #Production HT/CAS
[array]$UM = 'produ01','produ02' #Production UM
[array]$MB = 'prodm03','prodm04','prodm02','prodm01' #Production MB
[array]$DRHTCAS = 'drhc01','drhc02' #DR HT/CAS
[array]$DRUM = 'dru01' #DR UM
[array]$DRMB = 'drm01','drexm02' #DR MB</pre>
<p>Each server grouping is an array that the code body will loop through.  To accommodate a grouping that might only have one server, the variables are cast as arrays.</p>
<p>The next part of the variables section is for putting the servers into maintenance mode in SCOM.</p>
<pre class="brush: ps">#SCOM settings
$bUpdateSCOM = $true #Set to false if SCOM is not used or servers should not be put into maintenance mode
$rmsName = 'rmsserver' #SCOM RMS to connect to if SCOM update is true
$maintModeDuration = 90 #Maximum duration, in minutes, of SCOM maintenance window</pre>
<p>All of the variable declarations have trailing comments so you know what they are for, but they are also explained here.  Line 2 is for indicating whether you have SCOM in your environment or, even if you do, that you don&#8217;t want to execute that portion in the code body.  Line 3 is the name of the root management server to connect to with remote PowerShell.  Line 4 is for how long the server should be put into maintenance.  (When a server has finished rebooting and all is well, the server will be taken out of maintenance mode, so this is in case it can&#8217;t be done for some reason.)</p>
<p>Next is where non-mail variables are defined.</p>
<pre class="brush: ps">$WUCompDelay = 90 #Maximum minutes allowed for Windows Update to complete
$rebootMaxWait = 20 #Maximum minutes allowed for server to complete reboot
$serviceStartDelay = 3 #Minutes to wait for Exchange services to start after reboot
$serviceStartAttempts = 3 #Number of times to check for services started
$transcriptLog = 'c:\scripts\RebootScriptLog.txt' #Transcript file to record screen output and send in email</pre>
<p>Line 1 specifies how long the script should wait for Windows Update to complete before aborting.  Earlier versions had this set to 20 minutes, then 30, then 45, increasing mostly to accommodate roll-up installation since compiling .NET images (aka the Ngen process) can take some time to complete.  You can set this to whatever you feel comfortable with, knowing that if you set it too short the script will abort when there may be nothing wrong with the server.  Line 2 is how long to wait for a server to reboot before assuming something is wrong and aborting.  Line 3 is how long to wait after a server has successfully rebooted before checking for service status.  In other words, how much time should be given before checking that all Exchange services have started.  This is used along with the value for Line 4 to determine the total time to wait for services to start.  If any service isn&#8217;t started by the time the value for Line 3 has initially passed, the script will sleep for those number of minutes and then check again, repeating until all services are started or the number of checks in Line 4 have been reached.  Line 5 is the path and name of the log file to record the screen output to for later review.</p>
<p>The last part of the variables section is for notification.</p>
<pre class="brush: ps">#Mail settings
$mailSender = 'Server Reboot Script &lt;srs@company.com&gt;'
$mailServer = 'smtp.company.com' #SMTP FQDN to relay through
[array]$mailRecipients = 'user@company.com' #Recipients for notification and log file
$bSendStatusUpdates = $true #Set to false to not send status notification when each server is starting/ending
[array]$statusRecipients = 'phonenumber@SMSdomain.com' #Mobile recips for in-progress status updates</pre>
<p>Lines 2 and 3 are self-explanatory.  Line 4 is a comma-separated list of addresses to send the script completion notice and transcript log.  It is cast as an array in case you specify only one recipient.  Line 5 indicates whether you want status updates to be sent, typically to a mobile device.  These updates are formatted with SMS recipients in mind, so they are to the point.  An status update is sent when a server is starting to be processed, when it has successfully rebooted and passes health checks, and when the script is complete or has aborted (including why it aborted).  This is so the person executing the script doesn&#8217;t have to keep an eye on the script to know its progress.  Line 6 is a comma-separated list of address to receive the status updates.  Again, it is cast as an array if you only have one recipient.  While it is geared for consumption by a mobile device via SMS, you can use any address you want.</p>
<p>This completes the variables section.  The next post will skip the functions section and delve into the script body, covering the functions as they are called.  Download the complete script: Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Updated script to install Exchange 2010 prerequisites</title>
		<link>http://www.flobee.net/updated-script-to-install-exchange-2010-prerequisites/</link>
		<comments>http://www.flobee.net/updated-script-to-install-exchange-2010-prerequisites/#comments</comments>
		<pubDate>Wed, 14 Dec 2011 16:57:20 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=600</guid>
		<description><![CDATA[I have updated the script, first discussed in this post, then updated here and here, to account for new hotfixes since I last updated it.  The following is a list of changes: The list of required OS hotfixes is based on this TechNet page. The OS version code is less restrictive, the better to handle [...]]]></description>
			<content:encoded><![CDATA[<p>I have updated the script, first discussed in <a title="Script to install all Exchange 2010 prerequisites" href="http://www.flobee.net/script-to-install-all-exchange-2010-prerequisites/">this </a>post, then updated <a title="Exchange 2010 prerequisite installation script updated" href="http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated/">here</a> and <a title="Exchange 2010 prerequisite installation script updated" href="http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated-2/">here</a>, to account for new hotfixes since I last updated it.  The following is a list of changes:</p>
<ul>
<li>The list of required OS hotfixes is based on <a title="Exchange 2010 Prerequisites" href="http://technet.microsoft.com/en-us/library/bb691354.aspx" target="_blank">this</a> TechNet page.</li>
<li>The OS version code is less restrictive, the better to handle minor version updates that don&#8217;t change the fact that a server is R1 or R2.</li>
<li>The list of files to install uses hash tables instead of simple arrays.</li>
<li>Most of the hotfixes added require manually downloading because they require accepting a EULA or are only available through Premier or manual request from the KB article.  If one of these is not already downloaded, the script output will give the URL and then exit.  Even though the script will download missing hotfixes for you when applicable, the process will be smoother for you if you download the manual ones ahead of time.</li>
<li>Only the hotfixes for the specific version of Windows (R1, R2, Win7) are installed.  The TechNet page lists two hotfixes for Windows 7, but neither are installed on my workstation and they report they are not applicable when I attempt to install them.  Others report similar results, so I have remarked those two out.</li>
<li>Installing on R1 no longer requires the Exchange-supplied XML files for the OS features.  Like R2, the features to install are contained within the script.</li>
<li>Exchange 2010 SP2 requires that CAS has the &#8220;IIS WMI Compatibility&#8221; feature installed, so that has been added to the appropriate role selection.</li>
<li>Not listed on the TechNet page, but required for the UM role are the Unified Communications Managed API (version 2) and the Speech Server Platform Runtime.  Selecting the UM option will include installation of these.  The UC Managed API install uses a wizard because of ancillary programs that it may need to download (such as VC++ 2008 runtime), so it doesn&#8217;t support a silent install.</li>
<li>Other minor updates.</li>
</ul>
<p>I have done limited testing with updated version, but please let me know if you have any issues so I can address them.</p>
Note: There is a file embedded within this post, please visit this post to download the file.
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/updated-script-to-install-exchange-2010-prerequisites/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Patch and reboot script for an entire Exchange environment, Part 1</title>
		<link>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-1/</link>
		<comments>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-1/#comments</comments>
		<pubDate>Wed, 07 Dec 2011 22:29:17 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=596</guid>
		<description><![CDATA[This post is part of a multi-part series: Part 1 (this post) Part 2 Part 3 Part 4 Back in July, I posted a script that uses Microsoft Update to download and install hotfixes. That script is part of a bigger process that I use to patch all of my Exchange servers. I mentioned that [...]]]></description>
			<content:encoded><![CDATA[<p>This post is part of a multi-part series:</p>
<p style="padding-left: 30px;">Part 1 (this post)<br />
<a title="Patch and reboot script for an entire Exchange environment, Part 2" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-2/">Part 2</a><br />
<a title="Patch and reboot script for an entire Exchange environment, Part 3" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-3/">Part 3</a><br />
<a title="Patch and reboot script for an entire Exchange environment, Part 4" href="http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-4/">Part 4</a></p>
<p>Back in July, I <a title="Install Windows Updates" href="http://www.flobee.net/programmatically-run-windows-update-as-part-of-a-broader-patch-and-reboot-process/">posted</a> a script that uses Microsoft Update to download and install hotfixes. That script is part of a bigger process that I use to patch all of my Exchange servers. I mentioned that the script for that process was forthcoming. It is a big script, and I have yet to post it because I am always tweaking it. But if I wait for it to be code complete, I will never post it. So this is the first post of what will be several to document what it does.  This post will only cover the environment in which I am working and what the script requirements are.</p>
<p>First is the environment.  It is wholly Exchange 2010 (as of this writing, all servers are SP1 RU6) on Windows 2008 SP2.  There are over 8000 mailboxes, of which 1500 or so are shared.  There are no public folders.  Server and database infrastructure:</p>
<ul>
<li>Two data centers, one for production and one effectively for DR</li>
<li>Primary data center:</li>
<ul>
<li>2 HT/CAS servers behind a hardware load balancer (HLB)</li>
<li>2 UM servers</li>
<li>3 active MB servers</li>
<li>1 lag MB server</li>
</ul>
<li>Secondary data center (DR):</li>
<ul>
<li>2 HT/CAS servers behind an HLB</li>
<li>1 UM server</li>
<li>2 MB servers</li>
</ul>
<li>DAG properties</li>
<ul>
<li>Single DAG that spans both sites</li>
<li>22 databases</li>
<li>2 copies of each database across the 3 mailbox servers in the primary data center, e.g., DB1 is on server 1 and 2, DB2 is on server 2 and 3, etc.</li>
<li>All databases have a copy on the lag server</li>
<li>Each mailbox server in the secondary data center has a copy of every database</li>
<li>Each database has 5 copies total (including lag)</li>
</ul>
</ul>
<p>At my company, patch management is done by an application that has no awareness of Exchange, and reboots are simply staggered.  This is not an acceptable solution for patching an Exchange environment that has high availability, needs databases to be balanced after patching, and ideally best practices are followed for suspending DB replication before rebooting, etc.  As a result, these are the requirements for automated patching and rebooting of my Exchange environment:</p>
<ul>
<li>Process each server serially so that any problem that arises never affects more than one server, maintaining HA</li>
<li>Move active copies of databases to other servers so there is zero impact to clients</li>
<li>Suspend and resume replication of passive copies before and after reboots</li>
<li>Balance active databases when all mailbox servers are complete</li>
<li>Put servers in maintenance mode</li>
<li>Send status notifications to mobile device</li>
<li>Send detailed log when complete</li>
<li>Gracefully exit if any timeouts are reached (patching, rebooting, service starting, etc.)</li>
<li>Verify required services are started before moving on</li>
</ul>
<p>My script, currently at 445 lines, meets all of these requirements.  There are other things I intend to add to it, but I will go into detail after I have deconstructed the script for you in future posts.  Part 2 of this series, which will cover the overall script structure and its first section (variables), will be published soon.</p>
<p>Download the complete script: Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/patch-and-reboot-script-for-an-entire-exchange-environment-part-1/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Super duper delegate retrieval script</title>
		<link>http://www.flobee.net/super-duper-delegate-retrieval-script/</link>
		<comments>http://www.flobee.net/super-duper-delegate-retrieval-script/#comments</comments>
		<pubDate>Fri, 07 Oct 2011 17:56:56 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=581</guid>
		<description><![CDATA[Update 5/11/12: EWS 1.1 download link no longer valid, so the script has been updated to use 1.2. Update 3/13/12: The script has been updated to accommodate running in either EMS or PowerShell with remoting. Support for additional delegate retrieval errors has been added, as well as loading the Exchange snap-in directly if no cmdlets [...]]]></description>
			<content:encoded><![CDATA[<p><span style="color: #3366ff;">Update 5/11/12: EWS 1.1 download link no longer valid, so the script has been updated to use 1.2.</span></p>
<p><span style="color: #3366ff;">Update 3/13/12:  The script has been updated to accommodate running in either EMS or PowerShell with remoting.  Support for additional delegate retrieval errors has been added, as well as loading the Exchange snap-in directly if no cmdlets are already in the session.  Inline code and download have been updated.</span></p>
<p><span style="color: #3366ff;">Update 11/11/11: Minor update made to resolve a potential issue when translating user name to SID. Inline code and download have been updated.</span></p>
<p>Getting delegate information has always been tricky. Information is stored in multiple places, you have to use different tools to get that information, and business requirements can add to the data needed to paint a complete picture for a given delegate. Taking advantage of the <a title="EWS Managed API 1.2" href="http://www.microsoft.com/en-us/download/details.aspx?id=28952" target="_blank">EWS Managed API</a> to do the heavy lifting for information not easily exposed, I wrote this script to report delegate permissions and settings, mostly to help when troubleshooting why a delegate can&#8217;t do something or is getting weird behavior.</p>
<p>Using the <span id="codekeyword" class="flobeecode">GetDelegates()</span> method of the managed API not only retrieves the delegates and their permissions to the well-known folders, but also whether private items are visible, if meeting requests are sent to a specific delegate, and the meeting request handling of the owner. In reality, though, that isn&#8217;t enough information to give the total picture. Full mailbox access supersedes folder permissions, so knowing who has that is needed. My company uses the registry modifications that control where <a title="Control location for delegate who deletes items" href="http://support.microsoft.com/kb/202517" target="_blank">deleted items</a> and <a title="Control location for delegate who sends messages" href="http://support.microsoft.com/kb/972148" target="_blank">sent items</a>, which means a delegate needs permission to those folders, so getting that is needed. Lastly, delegates are granted send on behalf of permission (which, incidentally, <a title="Send on behalf of permission fails to be updated" href="http://support.microsoft.com/kb/2593557" target="_blank">isn&#8217;t even required</a> for a delegate to send a meeting request on behalf of the owner), but if a delegate has send as permission, that takes precedence when sending email messages, so knowing if a delegate has that right is also necessary.</p>
<p>The only required argument is an owner&#8217;s identity, such as email address, username, alias, etc. An optional switch parameter is <span id="codekeyword" class="flobeecode">-includeSendAs</span>. I made it an opt-in feature because getting the send as information from AD makes the entire script run over 12 times longer (in my company&#8217;s infrastructure): 1.7 seconds versus 20.7 seconds for an owner with five delegates. Although I prefer to not hard-code information that can be determined dynamically, I took the poor man&#8217;s route for the EWS URL, so you need to enter that on line 26. If you don&#8217;t care about any of the details of the script contents, you can simply stop reading, download it, and run with it.Note: There is a file embedded within this post, please visit this post to download the file.</p>
<p>Thanks to the managed API, retrieving the delegates can be done in seven lines (four, if you specify the DLL path when loading it instead of putting it in a variable, the same for setting the EWS URL, and if you specify the Exchange version directly when creating the service object).</p>
<pre class="brush:ps;gutter:false">$ewsuri=[system.URI]"https://owa.domain.com/ews/exchange.asmx"
$dllpath = "$env:ProgramFiles\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"
[void][Reflection.Assembly]::LoadFile($dllpath)
$exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion)
$service.Url = $ewsuri
$service.GetDelegates($owner,$true)</pre>
<p>After getting the delegates, full mailbox access and, optionally, send as are retrieved. Because of the added time to get send as, I added a progress bar while information is being retrieved. The delegate properties stored in the owner&#8217;s mailbox include email address, display name, and SID. The property returned for full mailbox access and send as is username. Since there isn&#8217;t a common property between the two collections, the usernames must be converted to SIDs:</p>
<pre class="brush:ps;gutter:false">function GetSID($acl)
	{
	$aSID = @()
	$acl | ForEach	{
		$adUser = [System.Security.Principal.NTAccount]($_.User.ToString())
		$aSID += $adUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
		}
	$aSID
	}</pre>
<p>Because of the registry changes to store items deleted from the owner&#8217;s mailbox in the owner&#8217;s Deleted Items folder, and to store items sent by the delegate from the owner in the owner&#8217;s Sent Items folder, Author permission or higher is needed for those folders. This is easy to do with <span id="codekeyword" class="flobeecode">Get-MailboxFolderPermission</span>.</p>
<p>Once all the information has been retrieved, you need to loop through the collection of delegates. When an orphaned delegate is on an owner&#8217;s mailbox, Exchange returns the delegate in the collection, but only as an error, not with any identifying information. I look for a matching error message and then direct you to the hidden message and property that lists all the delegates to find out which delegate is orphaned:</p>
<pre class="brush:ps;gutter:false">$delegates.DelegateUserResponses | ForEach {
	if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
		{
		Write-Output "*Orphaned Delegate"
		Write-Output "  Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml,"
		Write-Output "  property 0x684A101E to determine orphan entry."
		}</pre>
<p>If a delegate has a configuration error, such as not being listed in the publicDelegates attribute or in the Freebusy Data folder&#8217;s ACL, the delegate won&#8217;t be returned, so this specific error is noted.  And if any other error occurs retrieving a delegate, instead of just continuing and throwing errors later, a generic error will be noted:</p>
<pre class="brush:ps;gutter:false">
		elseif ($_.ErrorMessage -eq 'Delegate is not configured properly.')
			{
			Write-Output "*Misconfigured Delegate"
			Write-Output "  Delegate is missing from Freebusy Data folder or publicDelegates attribute."
			Write-Output "  Run delegation wizard in Outlook, edit delegate settings for user not"
			Write-Output "  listed in script output, and save in order to rewrite settings."
			}
		elseif ($_.Result -eq 'Error')
			{
			Write-Output "*Unknown Error with Delegate"
			Write-Output "  An error occurred retrieving the delegate, and there is not a response"
			Write-Output "  configured for it in this script."
			Write-Output "  Error message: $($_.ErrorMessage)"
			}</pre>
<p>To report whether a given delegate has full mailbox access or send as permission, the SID of the delegate is matched against the respective array from the <span id="codekeyword" class="flobeecode">GetSID()</span> function above:</p>
<pre class="brush:ps;gutter:false">if ($fmaSID -match $_.delegateuser.UserId.SID)
	{
	Write-Output "  Mailbox-level permission: Full Mailbox Access"
	}</pre>
<p>The collection includes the granted permission to the well-known folders exposed in Outlook&#8217;s delegation wizard. Since it is rare for someone to use or grant permission to Journal or Notes, rather than just echo the permission array, I display just the other four folders:</p>
<pre class="brush:ps;gutter:false">			Write-Output "  Folder permissions"
			Write-Output "    Calendar: $($_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString())"
			Write-Output "    Tasks: $($_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString())"
			Write-Output "    Inbox: $($_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString())"
			Write-Output "    Contacts: $($_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString())"</pre>
<p>To include the delegate&#8217;s potential permission to Deleted Items and Sent Items, I pass the respective folder permission object to the pipeline looking for a display name match:</p>
<pre class="brush:ps;gutter:false">[array]$delegateDIPerm = $deletedItemsPerm | where {$_.User -eq $delegateDisplayName}
if ($delegateDIPerm.Count -eq 1)
	{
	Write-Output "    Deleted Items: $($delegateDIPerm[0].AccessRights[0].ToString())"
	}
else
	{
	Write-Output "    Deleted Items: None"
	}</pre>
<p>Lastly, whether a delegate receives meeting requests or can view private items is a property of the individual delegate&#8217;s entry in the collection. How the owner wants meeting requests handled is not per delegate, so that setting is a property of the root collection. All of this is then output to the success stream, whether that is the screen (the default) or redirection to a file.</p>
<p>You can download the script from the link above, or copy the contents below (double-click in code area to highlight all).</p>
<pre class="brush:ps;collapse:true;auto-links:false">
<#
.Synopsis
	Display a mailbox's delegates and permissions.
.Description
	Retrieve the list of delegates for a mailbox and display the mailbox permission,
	folder permissions, meeting invite settings, and (optionally) whether the
	delegate has Send As permission.
.Parameter MailboxOwner
	Valid identity string of the user whose mailbox has the delegates.
.Parameter IncludeSendAs
	Switch to indicate that you want Send As permission to be included.
.Example
	get-delegates.ps1 user@domain.com -includesendas
.Example
	get-delegates.ps1 domain\username
.Notes
	Version: 1.3
	Date: 5/11/12
#>
Param (
	[Parameter(Position = 0,Mandatory = $true,HelpMessage="Identity of mailbox owner")][string]$mailboxOwner,
	[Alias("SA")][switch]$includeSendAs #Perform Send As lookup (takes longer)
	)

#Region Variables
$ewsuri=[system.URI]"https://owa.company.com/ews/exchange.asmx" #EWS URL
#EndRegion

#Region Functions
function LoadAPI
	{
	$dllpath = "$env:ProgramFiles\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"
	if (-not(Test-Path $dllpath))
		{
		Write-Output "This system does not have the EWS Managed API 1.2 installed, which is required to run this script."
		Write-Output "The API can be download at http://www.microsoft.com/download/en/details.aspx?id=28952"
		exit
		}
	[void][Reflection.Assembly]::LoadFile($dllpath)
	}

function GetSID($acl)
	{
	$aSID = @()
	$acl | ForEach	{
		$adUser = [System.Security.Principal.NTAccount]($_.User.ToString())
		$aSID += $adUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
		}
	$aSID
	}

function GetDelegates($owner)
	{
	$exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1
	$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion)
	$service.Url = $ewsuri
	$service.GetDelegates($owner,$true)
	}

function GetFMA($identity)
	{
	Get-MailboxPermission $identity | Where-Object {$_.IsInherited -eq $false}
	}

function GetSendAs($identity)
	{
	Get-ADPermission $identity | Where-Object {$_.IsInherited -eq $false -and $_.ExtendedRights -eq 'Send-As'}
	}

function GetFolderPermission($mailbox,$folder)
	{
	Get-MailboxFolderPermission "$mailbox`:\$folder"
	}
#EndRegion

#Region Body
#Connect/Load Exchange 2010 snap-in
$testcmd = Get-Command Get-Mailbox -ErrorAction SilentlyContinue
if (-not($testcmd)){Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010}

LoadAPI
try
	{
	$user = Get-User $mailboxOwner -Filter {RecipientType -eq 'UserMailbox'} -ErrorAction Stop
	}
catch
	{
	Write-Output "`"$mailboxOwner`" cannot be found or does not have a mailbox.  Verify the entered information."
	exit
	}

#Get list of delegates and permissions from EWS
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving Delegates" -PercentComplete 0
$delegates = GetDelegates $user.WindowsEmailAddress.ToString()

#Get list of users with full mailbox access
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving FMA List" -PercentComplete 40
$fullMailboxAccess = GetFMA $user.Identity
$fmaSID = GetSID $fullMailboxAccess #Convert username to SID
if ($includeSendAs)
	{
	#Get list of users with send as permission from AD
	Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving Send As List" -CurrentOperation "(This part takes the longest.)" -PercentComplete 70
	$sendAs = GetSendAs $user.Identity
	$saSID = GetSID $sendAs #Convert username to SID
	}
#Get permissions for additional folders
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving additional folder permissions" -PercentComplete 90
$deletedItemsPerm = GetFolderPermission $user.Identity 'Deleted Items'
$sentItemsPerm = GetFolderPermission $user.Identity 'Sent Items'

Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Completed $true

Write-Output "`n$($user.FirstName) $($user.LastName) has $($delegates.DelegateUserResponses.Count) delegate(s)"
#Loop through list of delegates
if ($delegates.DelegateUserResponses.Count -gt 0)
	{
	Write-Output "`nGlobal Delegate Meeting Request Handling: $($delegates.MeetingRequestsDeliveryScope)"
	$delegates.DelegateUserResponses | ForEach {
		Write-Output `n
		#Delegate account deleted in AD but still listed in list
		if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
			{
			Write-Output "*Orphaned Delegate"
			Write-Output "  Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml,"
			Write-Output "  property 0x684A101E to determine orphan entry."
			}
		elseif ($_.ErrorMessage -eq 'Delegate is not configured properly.')
			{
			Write-Output "*Misconfigured Delegate"
			Write-Output "  Delegate is missing from Freebusy Data folder or publicDelegates attribute."
			Write-Output "  Run delegation wizard in Outlook, edit delegate settings for user not"
			Write-Output "  listed in script output, and save in order to rewrite settings."
			}
		elseif ($_.Result -eq 'Error')
			{
			Write-Output "*Unknown Error with Delegate"
			Write-Output "  An error occurred retrieving the delegate, and there is not a response"
			Write-Output "  configured for it in this script."
			Write-Output "  Error message: $($_.ErrorMessage)"
			}
		else
			{
			$delegateDisplayName = $_.delegateuser.userid.displayname
			Write-output "*$delegateDisplayName `($($_.delegateuser.userid.primarysmtpaddress)`)"
			if ($fmaSID -match $_.delegateuser.UserId.SID)
				{
				Write-Output "  Mailbox-level permission: Full Mailbox Access"
				}
			else
				{
				Write-Output "  Mailbox-level permission: None"
				}
			if ($includeSendAs)
				{
				if ($saSID -match $_.delegateuser.UserId.SID)
					{
					Write-Output "  Send As permission: True"
					}
				else
					{
					Write-Output "  Send As permission: False"
					}
				}
			Write-Output "  Folder permissions"
			Write-Output "    Calendar: $($_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString())"
			Write-Output "    Tasks: $($_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString())"
			Write-Output "    Inbox: $($_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString())"
			Write-Output "    Contacts: $($_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString())"
			[array]$delegateDIPerm = $deletedItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
			if ($delegateDIPerm.Count -eq 1)
				{
				Write-Output "    Deleted Items: $($delegateDIPerm[0].AccessRights[0].ToString())"
				}
			else
				{
				Write-Output "    Deleted Items: None"
				}
			[array]$delegateSIPerm = $sentItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
			if ($delegateSIPerm.Count -eq 1)
				{
				Write-Output "    Sent Items: $($delegateSIPerm[0].AccessRights[0].ToString())"
				}
			else
				{
				Write-Output "    Sent Items: None"
				}

			Write-Output "  Receives Meeting Requests: $($_.delegateuser.receivecopiesofmeetingmessages)"
			Write-Output "  Can View Private Items: $($_.delegateuser.viewprivateitems)"
			}
		}
	}
Write-Output `n
#EndRegion
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/super-duper-delegate-retrieval-script/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Programmatically run Windows Update (as part of a broader patch and reboot process)</title>
		<link>http://www.flobee.net/programmatically-run-windows-update-as-part-of-a-broader-patch-and-reboot-process/</link>
		<comments>http://www.flobee.net/programmatically-run-windows-update-as-part-of-a-broader-patch-and-reboot-process/#comments</comments>
		<pubDate>Wed, 13 Jul 2011 20:59:59 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=569</guid>
		<description><![CDATA[It&#8217;s been awhile since I have posted a script, so this is the first post of my process for patching and rebooting the Exchange servers at work. I needed a way to patch the environment while maintaining high availability and not resorting to just staggering reboots at, say, 30-minute intervals. The staggered approach doesn&#8217;t account [...]]]></description>
			<content:encoded><![CDATA[<p>It&#8217;s been awhile since I have posted a script, so this is the first post of my process for patching and rebooting the Exchange servers at work.  I needed a way to patch the environment while maintaining high availability and not resorting to just staggering reboots at, say, 30-minute intervals.  The staggered approach doesn&#8217;t account for any issues that can occur if a server doesn&#8217;t come back up or services fail to start, etc.  What I have done to maintain HA starts with this script.</p>
<p>I originally intended to have the Windows Update process as part of my bigger patch-and-reboot script, but I learned that you can&#8217;t make calls to the Windows Update API from remote systems.  This means that I need to execute the script locally on each server, but my reboot script runs remotely from a management server.  Therefore, this is just a standalone script for installing software updates from Windows Update (or, in my case, Microsoft Update, since that is installed).  It is locally installed as an on-demand Scheduled Task that gets remotely called from my reboot script, but I will go into those details when I post about that script.</p>
<p>The first function in the script is for writing to the event log.  This is how my reboot script monitors the progress.  I create a custom event source so that there is no chance of conflicting with anything else.  Any time something is to be written I pass the message, type (Information, Warning, etc.), and event ID.</p>
<pre class="brush:ps">
function WriteEvent ($eventMessage,$eventType,$eventID)
	{
	$sourceName = 'UCTeam Scripts'
	if (-not([System.Diagnostics.EventLog]::SourceExists($sourceName)))
		{
		[System.Diagnostics.EventLog]::CreateEventSource($sourceName,'Application')
		}
	$EventLog = New-Object System.Diagnostics.EventLog('Application')
	$eventLogType = [System.Diagnostics.EventLogEntryType]::$eventType
	$EventLog.Source = $sourceName
	$EventLog.WriteEntry($eventMessage,$eventLogType,$EventID)
	}
</pre>
<p>The search criteria is for software updates (no drivers) that are not installed and are checked by default.  If you want to control which updates to install, you can manipulate the search criteria a little bit (as <a href="http://msdn.microsoft.com/en-us/library/aa386526%28v=VS.85%29.aspx" title="Search method documentation">documented</a>) so only certain updates are returned, but to not install an update associated with a specific KB article, you have to loop through the results and then not install the update that matches that property.</p>
<pre class="brush:ps">
#Software updates only, selected by default, not already installed
$criteria="IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
WriteEvent 'Windows Update process is starting.' 'Information' '1000'
WriteEvent "Beginning check for available updates based on the following criteria: $criteria." 'Information' '1001'
$updates = $updateSession.CreateupdateSearcher().Search($criteria).Updates
</pre>
<p>I use the <span class="flobeecode" id="codekeyword">WriteEvent</span> function to write status updates to the event log so that my remote script can watch for events that indicate when it is done or that something has gone wrong.  After the search results are downloaded, you still have to actually download them.  Since I am not filtering any specific updates, I specify the entire <span class="flobeecode" id="codevariable">$updates</span> object to download.</p>
<pre class="brush:ps">
#Create download object
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updates
WriteEvent 'Beginning download of available updates.' 'Information' '1002'
$result= $downloader.Download()
</pre>
<p>When that is complete you loop through the collection and add them to a new installer object, which is finally used to installed the updates.</p>
<pre class="brush:ps">
$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
$updates | Where-Object {$_.isdownloaded} | Foreach-Object {$updatesToInstall.Add($_) | Out-Null}
#Create installer object
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $updatesToInstall
WriteEvent "Beginning installation of downloaded updates `($($installer.Updates.count)`)." 'Information' '1003'
#Run installation of downloaded files
$installationResult = $installer.Install()
</pre>
<p>Lastly, I loop through the results to log the name of every update that installed (or tried to install).</p>
<pre class="brush:ps">
$global:counter=-1
$installResults = $installer.updates | Select-Object -property Title,EulaAccepted,@{label='Result'; `
	expression={$resultCode[$installationResult.GetUpdateResult($($global:counter++)).resultCode]}}
WriteEvent ($installResults | Format-Table -Wrap | Out-String) 'Information' '1002'
</pre>
<p>While you can have the script issue a reboot locally, I just log when the updates are complete so that my reboot script can trigger that an know precisely when that occurs.  The entire script is below (since the code snippets above don&#8217;t include every line), but you can also download it.Note: There is a file embedded within this post, please visit this post to download the file.</p>
<pre class="brush:ps">
#Download and install software updates from WU that are selected by default
#v1.0 11/4/2010

function WriteEvent ($eventMessage,$eventType,$eventID)
	{
	$sourceName = 'UCTeam Scripts'
	if (-not([System.Diagnostics.EventLog]::SourceExists($sourceName)))
		{
		[System.Diagnostics.EventLog]::CreateEventSource($sourceName,'Application')
		}
	$EventLog = New-Object System.Diagnostics.EventLog('Application')
	$eventLogType = [System.Diagnostics.EventLogEntryType]::$eventType
	$EventLog.Source = $sourceName
	$EventLog.WriteEntry($eventMessage,$eventLogType,$EventID)
	}

#Software updates only, selected by default, not already installed
$criteria="IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
WriteEvent 'Windows Update process is starting.' 'Information' '1000'
WriteEvent "Beginning check for available updates based on the following criteria: $criteria." 'Information' '1001'
$updates = $updateSession.CreateupdateSearcher().Search($criteria).Updates
if ($updates.Count -eq 0)
	{
	WriteEvent 'Check for available updates is complete.  There are no updates to apply.' 'Information' '1001'
	}
else
	{
	WriteEvent "Check for available updates is complete.  There are $($updates.Count) updates to apply." 'Information' '1001'
	#Create download object
	$downloader = $updateSession.CreateUpdateDownloader()
	$downloader.Updates = $Updates
	WriteEvent 'Beginning download of available updates.' 'Information' '1002'
	$result= $downloader.Download()
	if (($result.Hresult -eq 0) –and (($result.resultCode –eq 2) -or ($result.resultCode –eq 3)))
		{
		WriteEvent 'Download of available updates has completed.' 'Information' '1002'
		$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
		$updates | Where-Object {$_.isdownloaded} | Foreach-Object {$updatesToInstall.Add($_) | Out-Null}
		#Create installer object
		$installer = $updateSession.CreateUpdateInstaller()
		$installer.Updates = $updatesToInstall
		WriteEvent "Beginning installation of downloaded updates `($($installer.Updates.count)`)." 'Information' '1003'
        #Run installation of downloaded files
		$installationResult = $installer.Install()
        $global:counter=-1
        $installResults = $installer.updates | Select-Object -property Title,EulaAccepted,@{label='Result'; `
			expression={$resultCode[$installationResult.GetUpdateResult($($global:counter++)).resultCode]}}
		WriteEvent ($installResults | Format-Table -Wrap | Out-String) 'Information' '1002'
		}
	else
		{
		WriteEvent 'Error downloading updates.' 'Warning' '1001'
		}
	}
WriteEvent 'Windows Update process is complete.' 'Information' '1010'

#Reboot
#Restart-Computer -Force -Confirm:$false
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/programmatically-run-windows-update-as-part-of-a-broader-patch-and-reboot-process/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Use the Exchange Management Shell or LDAP to get a list of quarantined mobile devices</title>
		<link>http://www.flobee.net/get-a-list-of-quarantined-mobile-devices/</link>
		<comments>http://www.flobee.net/get-a-list-of-quarantined-mobile-devices/#comments</comments>
		<pubDate>Mon, 06 Jun 2011 15:23:14 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Windows Mobile]]></category>
		<category><![CDATA[Windows Phone]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=558</guid>
		<description><![CDATA[You can use Exchange Control Panel to view the list of Exchange ActiveSync devices that are in a quarantined state.  I wanted to be able to get this list without using ECP, but I didn&#8217;t know where this information is stored: Exchange or AD?  Long story short, it is stored in AD.  Every Exchange ActiveSync [...]]]></description>
			<content:encoded><![CDATA[<p>You can use Exchange Control Panel to view the list of Exchange ActiveSync devices that are in a quarantined state.  I wanted to be able to get this list without using ECP, but I didn&#8217;t know where this information is stored: Exchange or AD?  Long story short, it is stored in AD.  Every Exchange ActiveSync partnership exists as an AD object (whose class is msExchActiveSyncDevice) located as a  child object of the user object whose mailbox has the partnership.  The access state of the device is stored as an integer in the msExchDeviceAccessState attribute of the object.</p>
<p>To use EMS to get the list, run this command:</p>
<pre class="brush:ps;gutter:false">Get-ActiveSyncDevice -filter {deviceaccessstate -eq 'quarantined'}</pre>
<p>If you want to use LDAP to get the list, this is the corresponding search filter:</p>
<p>(&amp;(objectclass=msexchactivesyncdevice)(msexchdeviceaccessstate=3))</p>
<p>The device access state values are 1 for allowed, 2 for blocked, 3 for quarantined.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/get-a-list-of-quarantined-mobile-devices/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>TechEd 2011: WP7 Mango update a snoozer</title>
		<link>http://www.flobee.net/teched-2011-wp7-mango-update-a-snoozer/</link>
		<comments>http://www.flobee.net/teched-2011-wp7-mango-update-a-snoozer/#comments</comments>
		<pubDate>Wed, 18 May 2011 14:43:04 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[Lync]]></category>
		<category><![CDATA[Windows Phone]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=555</guid>
		<description><![CDATA[Microsoft announced more details of the Windows Phone 7 update (Mango), due out later this year.  They hyped the enterprise features, which are nothing to be proud of, IMO.  Support for searching you server-side mailbox.  Already in WM 6.x?  Check.  Support for IRM.  Already in WM 6.x?  Check.  Lync client. Communicator Mobile for WM 6.x [...]]]></description>
			<content:encoded><![CDATA[<p>Microsoft announced more details of the Windows Phone 7 update (Mango), due out later this year.  They hyped the enterprise features, which are nothing to be proud of, IMO.  Support for searching you server-side mailbox.  Already in WM 6.x?  Check.  Support for IRM.  Already in WM 6.x?  Check.  Lync client. Communicator Mobile for WM 6.x to connect to OCS already?  Check.  A mobile client for Lync should have been released with Lync server RTM.  Conversation views.  Already in WM 6.x?  Check.</p>
<p>Where is the at-rest device encryption?  It is already a cliche to say that it is ironic when Microsoft&#8217;s own mobile OS doesn&#8217;t support all of their Exchange ActiveSync policies, one of them being the very thing whose absence will keep the OS out of the enterprise.  Because my company deals with PHI/PII, we require at-rest encryption, which means the only devices that we allow via EAS are iDevices, Android with Touchdown installed, and Windows Mobile 6.x.</p>
<p>One may argue that WP7 is a consumer device.  But MS has abandoned everything for WM 6.x.  They may say that WP7 is a consumer device, but they treat as their one-and-only OS for home and business.  They didn&#8217;t even release a TechEd app for WM for 6.x, only WP7 (and Android).  So if you release an app for 10,000 enterprise geeks at your premier technical conference for IT Pros and Developers on only your consumer-targeted mobile OS, what message are you really sending about who you target audience is?</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/teched-2011-wp7-mango-update-a-snoozer/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Waiting for TechEd 2011 keynote to start</title>
		<link>http://www.flobee.net/waiting-for-teched-2011-keynote-to-start/</link>
		<comments>http://www.flobee.net/waiting-for-teched-2011-keynote-to-start/#comments</comments>
		<pubDate>Mon, 16 May 2011 12:58:44 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=551</guid>
		<description><![CDATA[Who is The Glitch Mob?  I don&#8217;t know, but they are making cool beats with their hardware beatmakers right now on stage.  Keynote will start soon.  Hope I don&#8217;t fall asleep.]]></description>
			<content:encoded><![CDATA[<p>Who is The Glitch Mob?  I don&#8217;t know, but they are making cool beats with their hardware beatmakers right now on stage.  Keynote will start soon.  Hope I don&#8217;t fall asleep.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/waiting-for-teched-2011-keynote-to-start/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>The correct way to restrict users to specific devices with Exchange ActiveSync</title>
		<link>http://www.flobee.net/the-correct-way-to-restrict-users-to-specific-devices-with-exchange-activesync/</link>
		<comments>http://www.flobee.net/the-correct-way-to-restrict-users-to-specific-devices-with-exchange-activesync/#comments</comments>
		<pubDate>Tue, 01 Mar 2011 17:58:49 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[Windows Mobile]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=541</guid>
		<description><![CDATA[One way to control access to a mailbox with Exchange ActiveSync is to restrict a user to a specific device ID (or more than one), as described in this TechNet help page. The prerequisite listed on the page is only that the user be enabled for EAS. You will find, however, that if you list [...]]]></description>
			<content:encoded><![CDATA[<p>One way to control access to a mailbox with Exchange ActiveSync is to restrict a user to a specific device ID (or more than one), as described in <a href="http://technet.microsoft.com/en-us/library/bb232080.aspx">this </a>TechNet help page.  The prerequisite listed on the page is only that the user be enabled for EAS.  You will find, however, that if you list one or more device IDs in the ActiveSyncAllowedDevicesIDs property and attempt to sync with a device that is not in the list, it will still be able to.</p>
<p style="text-align: left;">The reason for this is not explained clearly in Microsoft documentation, but can be inferred by <a href="http://msexchangeteam.com/archive/2010/11/15/456931.aspx">this</a> good post on the Exchange blog.  It details the flow logic when a device is accessing a mailbox and the order in which permissive and preventative access is handled.  This image from the post sums up the flow:</p>
<p style="text-align: left;"><a title="Device access flow" href="http://www.flobee.net/images/ABQ-PermissiveOrg.png"><img class="aligncenter" title="Device access flow" src="http://www.flobee.net/images/ABQ-PermissiveOrg.png" alt="" width="299" height="163" /></a></p>
<p style="text-align: left;">When a device attempts to sync with a mailbox, the Allow/Block list on the user object is checked first, followed by any device access rules defined at the org level, and then the global access rule (the decision point labeled as Anything Unknown).  The default configuration at the org level is a permissive organization, where any device not explicitly blocked at the user level or does not match any device access rule is allowed.</p>
<p style="text-align: left;">The important distinction, however, is that specifying an allowed device ID at the user level is not exclusive, while specifying a blocked device ID is [effectively] exclusive.  This means that the access logic doesn&#8217;t stop at the user level when an allowed device ID is specified, though it does when a blocked device ID is specified.  Therefore, in a default permissive organization, specifying an allowed device ID for a user does not restrict the user to only that device.  In order to restrict a user to the device ID(s) specified on the user object, it is also necessary to define a device access rule and/or change the global access policy so that a device will otherwise be blocked (or quarantined).</p>
<p style="text-align: left;">To understand the action taken by Exchange when a device attempts to synchronize, I have made my own flowchart (click to zoom):</p>
<p style="text-align: left;"><a title="EAS Device Access Flow" href="http://www.flobee.net/images/EASDeviceAccessFlowchart.png"><img class="aligncenter" title="EAS Device Access Flow" src="http://www.flobee.net/images/EASDeviceAccessFlowchart.png" alt="" width="319" height="296" /></a></p>
<p style="text-align: left;">
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/the-correct-way-to-restrict-users-to-specific-devices-with-exchange-activesync/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Script to change security DL ownership</title>
		<link>http://www.flobee.net/script-to-change-security-dl-ownership/</link>
		<comments>http://www.flobee.net/script-to-change-security-dl-ownership/#comments</comments>
		<pubDate>Tue, 11 Jan 2011 23:51:07 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=531</guid>
		<description><![CDATA[Exchange 2010 implements a change to managing security distribution lists, especially in SP1. When using the Exchange Management Console (EMC) in the RTM version, changes made to membership or ownership of a security distribution list are successful when you are not the owner. This is because a parameter of the Set-DistributionGroup cmdlet is tacitly added [...]]]></description>
			<content:encoded><![CDATA[<p>Exchange 2010 implements a change to managing security distribution lists, especially in SP1.  When using the Exchange Management Console (EMC) in the RTM version, changes made to membership or ownership of a security distribution list are successful when you are not the owner.  This is because a parameter of the <span id="codekeyword" class="flobeecode">Set-DistributionGroup</span> cmdlet is tacitly added that allows someone with the RBAC management role of Security Group Creation and Membership to update the DL.  Starting with Exchange 2010 SP1, the console no longer adds this parameter (<span id="codekeyword" class="flobeecode">-BypassSecurityGroupManagerCheck</span>).</p>
<p>The result is that, despite being an administrator (by being in the Organization Management role), if you are not explicitly listed as an owner of a security DL, you will not be able to makes changes to ownership or members via the console&#8230;period.  This goes against the functionality that has existed in the Exchange console since 1996.  And better yet, Microsoft says this is by design.  The fact that it worked in RTM is the bug, not the other way around in SP1.  I think that is one of the most boneheaded changes made in 2010.  The solution is to either add the administrators as explicit owners or have them use the shell and manually add <span id="codekeyword" class="flobeecode">-BypassSecurityGroupManagerCheck</span>.</p>
<p>Both options are impractical.  If your help desk manages DLs you can&#8217;t just add all members as owners.  For one thing, even though Exchange 2010 supports multiple owners, Outlook (even 2010) only displays the first entry.  This means end users won&#8217;t know who to contact because the listed owners could be one or two &#8220;real&#8221; owners and all the rest are help desk.  The other option, using the shell, isn&#8217;t the easiest for anyone that isn&#8217;t a dedicated Exchange admin.</p>
<p>My solution for the help desk staff is a PowerShell script that uses Windows forms to provide a GUI that lets you search for a DL, display the ownership, add and remove owners, then apply the changes.  It includes username validation so it won&#8217;t let you add a user that isn&#8217;t mailbox-enabled.</p>
<p>The one place I struggled is connecting to Exchange.  The script checks for the Exchange cmdlets being in memory.  If they are not, it will connect remotely to a designated server.  Because that is done when the form is activated, it causes the painting of the fields and labels to be delayed until the connection is complete.  I tried to find a way to wait until the painting is complete before connecting, but you can&#8217;t detect that when the fields are drawn by Windows directly and not the application.</p>
<p style="text-align: center;"><img src="http://www.flobee.net/graphics/ChangeDLOwner.png" alt="Change DL Owner screenshot" width="358" height="401" /></p>
<p>The code is listed here, but you can also download a zip file of it: Note: There is a file embedded within this post, please visit this post to download the file.</p>
<pre class="brush:ps">#Change ownership of Exchange 2010 SP1 distribution list
#Author: Scott Bueffel, http://www.flobee.net
#v1.0 1/6/10

$connectToServer = 'ServerName' #Server to connect to if shell doesn't have cmdlets in memory
$DLPrefix = '[DL] ' #Display name standard prefix.  If none, leave as two single-quotes

function ConnectToExchange
	{
	$testcmd = gcm Get-Mailbox -ErrorAction SilentlyContinue
	if (-not($testcmd))
		{
		$statusBarPanel1.Text = 'Connecting to Exchange...'
		#Connect remotely to Exchange
		$global:exsession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$connectToServer/PowerShell" -Name exchange
		Import-PSSession $exsession -AllowClobber -DisableNameChecking | Out-Null
		}
	$statusBarPanel1.Text = 'Connected to Exchange'
	}

#region Import the Assemblies
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
#endregion

#region Generated Form Objects
$form = New-Object System.Windows.Forms.Form
$txtAddOwner = New-Object System.Windows.Forms.TextBox
$label7 = New-Object System.Windows.Forms.Label
$label6 = New-Object System.Windows.Forms.Label
$btnRemoveOwner = New-Object System.Windows.Forms.Button
$btnAddOwner = New-Object System.Windows.Forms.Button
$lstOwners = New-Object System.Windows.Forms.ListBox
$label2 = New-Object System.Windows.Forms.Label
$label1 = New-Object System.Windows.Forms.Label
$btnGetOwner = New-Object System.Windows.Forms.Button
$statusBar1 = New-Object System.Windows.Forms.StatusBar
$btnCancel = New-Object System.Windows.Forms.Button
$btnApply = New-Object System.Windows.Forms.Button
$label5 = New-Object System.Windows.Forms.Label
$txtDLName = New-Object System.Windows.Forms.TextBox
$label4 = New-Object System.Windows.Forms.Label
$label3 = New-Object System.Windows.Forms.Label
$toolTip1 = New-Object System.Windows.Forms.ToolTip
$statusBarPanel1 = New-Object System.Windows.Forms.StatusBarPanel
$statusBarPanel2 = New-Object System.Windows.Forms.StatusBarPanel
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
$errorprovider = New-Object System.Windows.Forms.ErrorProvider
#endregion Generated Form Objects

function DisplayOwners ($DL)
	{
	$lstOwners.BeginUpdate()
	[array]$DLOwners = $DL.ManagedBy
	foreach ($owner in $DLOwners)
		{
		$lstOwners.Items.Add($owner.ToString().Substring($owner.ToString().LastIndexOf('/') + 1))
		}
	$lstOwners.EndUpdate()
	$txtAddOwner.Enabled = $true
	}

function RetrieveDL
	{
	$lstOwners.Items.Clear() #Remove any entries in owner list from previous query
	$btnAddOwner.Enabled = $false
	$txtAddOwner.Enabled = $false
	$statusBarPanel2.Text = 'Searching for DL...'
	$DLDisplayName = "$($DLPrefix)$($txtDLName.Text)"
	$script:DL = Get-DistributionGroup $DLDisplayName -ErrorAction SilentlyContinue
	if (!($DL))
		{
    	$errorProvider.SetError($txtDLName, 'The DL cannot be found.')
		$statusBarPanel2.Text = 'DL not found.'
    	return
  		}
  	#Valid DL returned
  	$errorProvider.SetError($txtDLName, '')
	DisplayOwners $DL
	$statusBarPanel2.Text = ''
	$btnCancel.Text = 'Cancel'
	}

function AddOwner_OnClick ($username)
	{
	$statusBarPanel2.Text = 'Validating user...'
	$ownerMailbox = Get-Mailbox $username -ErrorAction SilentlyContinue
	if (!($ownerMailbox))
		{
		$errorprovider.SetIconAlignment($txtAddOwner,2)
		$errorprovider.SetError($txtAddOwner, 'The user cannot be found.')
		$statusBarPanel2.Text = 'User not found.'
		return
		}
	elseif ($lstOwners.Items.Contains($username))
		{
		$errorprovider.SetIconAlignment($txtAddOwner,2)
		$errorprovider.SetError($txtAddOwner, 'The user is already in the list.')
		$statusBarPanel2.Text = 'User already is owner.'
		return
		}
	#Valid user found
	$lstOwners.Items.Insert(0,$username) #Add owner to top of list
	$statusBarPanel2.Text = 'User added.'
	$errorprovider.SetIconAlignment($txtAddOwner,3)
	$errorprovider.SetError($txtAddOwner,'')
	$txtAddOwner.Text = ''
	$btnApply.Enabled = $true
	}

function ToggleAddButton ($username)
	{
	if ($username.Length -gt 0)
		{
		$btnAddOwner.Enabled = $true
		}
	else
		{
		$btnAddOwner.Enabled = $false
		}
	}

function RemoveOwner_OnClick ($owner)
	{
	$lstOwners.Items.Remove($owner)
	$btnRemoveOwner.Enabled = $false
	$btnApply.Enabled = $true
	}

function ApplyChanges_OnClick
	{
	[array]$newOwners = $lstOwners.Items
	try
		{
		Set-DistributionGroup $($DL.DisplayName) -ManagedBy $newOwners -BypassSecurityGroupManagerCheck -ErrorAction Stop
		$statusBarPanel2.Text = 'DL ownership applied.'
		$errorprovider.SetError($btnApply, '')
		$btnCancel.Text = 'Close'
		$btnApply.Enabled = $false
		}
	catch
		{
		$statusBarPanel2.Text = 'Error occurred updating DL.'
		$errorprovider.SetError($btnApply, "Error occurred when updating the DL's new ownership.")
		}
	}

$OnLoadForm_StateCorrection = {$form.WindowState = $InitialFormWindowState}

#region Generated Form Code
$form.CancelButton = $btnCancel
$form.Text = "Change Distribution List Owner"
$form.Name = "form"
$form.KeyPreview = $True
$form.StartPosition = 1
$form.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 342
$System_Drawing_Size.Height = 363
$form.ClientSize = $System_Drawing_Size

$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 84
$System_Drawing_Size.Height = 20
$txtAddOwner.Size = $System_Drawing_Size
$txtAddOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$txtAddOwner.Text = "Enter username"
$txtAddOwner.Name = "txtAddOwner"
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 211
$txtAddOwner.Location = $System_Drawing_Point
$txtAddOwner.Enabled = $False
$txtAddOwner.TabIndex = 15
$txtAddOwner.add_Enter({if ($txtAddOwner.Text -eq 'Enter username') {$txtAddOwner.Text = ''}})
$txtAddOwner.add_KeyUp({ToggleAddButton $txtAddOwner.Text})
$txtAddOwner.add_KeyDown({if ($_.KeyCode -eq "Enter"){AddOwner_OnClick $txtAddOwner.Text}})

$form.Controls.Add($txtAddOwner)

$label7.TabIndex = 12
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 20
$System_Drawing_Size.Height = 23
$label7.Size = $System_Drawing_Size
$label7.Text = "2."
$label7.Font = New-Object System.Drawing.Font("Arial",9,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 13
$System_Drawing_Point.Y = 108
$label7.Location = $System_Drawing_Point
$label7.DataBindings.DefaultDataSourceUpdateMode = 0
$label7.Name = "label7"

$form.Controls.Add($label7)

$label6.TabIndex = 13
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 283
$System_Drawing_Size.Height = 35
$label6.Size = $System_Drawing_Size
$label6.Text = "Add and remove owners as necessary, then click Apply Changes to update the DL's ownership."
$label6.Font = New-Object System.Drawing.Font("Arial",8,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 109
$label6.Location = $System_Drawing_Point
$label6.DataBindings.DefaultDataSourceUpdateMode = 0
$label6.Name = "label6"

$form.Controls.Add($label6)

$btnRemoveOwner.TabIndex = 7
$btnRemoveOwner.Name = "btnRemoveOwner"
$btnRemoveOwner.Enabled = $False
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnRemoveOwner.Size = $System_Drawing_Size
$btnRemoveOwner.UseVisualStyleBackColor = $True

$btnRemoveOwner.Text = "Remove"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 252
$btnRemoveOwner.Location = $System_Drawing_Point
$btnRemoveOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$btnRemoveOwner.add_Click({RemoveOwner_OnClick $lstOwners.SelectedItem})

$form.Controls.Add($btnRemoveOwner)

$btnAddOwner.TabIndex = 6
$btnAddOwner.Name = "btnAddOwner"
$btnAddOwner.Enabled = $False
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnAddOwner.Size = $System_Drawing_Size
$btnAddOwner.UseVisualStyleBackColor = $True

$btnAddOwner.Text = "Add"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 181
$btnAddOwner.Location = $System_Drawing_Point
$btnAddOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$btnAddOwner.add_Click({AddOwner_OnClick $txtAddOwner.Text})

$form.Controls.Add($btnAddOwner)

$lstOwners.FormattingEnabled = $True
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 141
$System_Drawing_Size.Height = 134
$lstOwners.Size = $System_Drawing_Size
$lstOwners.DataBindings.DefaultDataSourceUpdateMode = 0
$lstOwners.Name = "lstOwners"
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 120
$System_Drawing_Point.Y = 155
$lstOwners.Location = $System_Drawing_Point
$lstOwners.TabIndex = 5
$lstOwners.add_SelectedIndexChanged({$btnRemoveOwner.Enabled = $true})

$form.Controls.Add($lstOwners)

$label2.TabIndex = 10
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 21
$System_Drawing_Size.Height = 23
$label2.Size = $System_Drawing_Size
$label2.Text = "1."
$label2.Font = New-Object System.Drawing.Font("Arial",9,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 12
$System_Drawing_Point.Y = 12
$label2.Location = $System_Drawing_Point
$label2.DataBindings.DefaultDataSourceUpdateMode = 0
$label2.Name = "label2"

$form.Controls.Add($label2)

$label1.TabIndex = 11
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 288
$System_Drawing_Size.Height = 32
$label1.Size = $System_Drawing_Size
$label1.Text = "Enter the display name of the DL and click Get Owners to display the ownership."
$label1.Font = New-Object System.Drawing.Font("Arial",8,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 13
$label1.Location = $System_Drawing_Point
$label1.DataBindings.DefaultDataSourceUpdateMode = 0
$label1.Name = "label1"

$form.Controls.Add($label1)

$btnGetOwner.TabIndex = 3
$btnGetOwner.Name = "btnGetOwner"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnGetOwner.Size = $System_Drawing_Size
$btnGetOwner.UseVisualStyleBackColor = $True

$btnGetOwner.Text = "Get Owners"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 120
$System_Drawing_Point.Y = 77
$btnGetOwner.Location = $System_Drawing_Point
$btnGetOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$btnGetOwner.add_Click({RetrieveDL})
$form.Controls.Add($btnGetOwner)

$statusBar1.ShowPanels = $True
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 342
$System_Drawing_Size.Height = 22
$statusBar1.Size = $System_Drawing_Size
$statusBar1.TabIndex = 14
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 0
$System_Drawing_Point.Y = 341
$statusBar1.Location = $System_Drawing_Point
$statusBar1.DataBindings.DefaultDataSourceUpdateMode = 0
$statusBar1.Name = "statusBar1"
$statusBar1.Panels.Add($statusBarPanel1)|Out-Null
$statusBar1.Panels.Add($statusBarPanel2)|Out-Null

$form.Controls.Add($statusBar1)

$btnCancel.TabIndex = 9
$btnCancel.Name = "btnCancel"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnCancel.Size = $System_Drawing_Size
$btnCancel.UseVisualStyleBackColor = $True

$btnCancel.Text = "Cancel"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 198
$System_Drawing_Point.Y = 302
$btnCancel.Location = $System_Drawing_Point
$btnCancel.DataBindings.DefaultDataSourceUpdateMode = 0
$btnCancel.DialogResult = 2

$form.Controls.Add($btnCancel)

$btnApply.TabIndex = 8
$btnApply.AutoSize = $True
$btnApply.Name = "btnApply"
$btnApply.Enabled = $False
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 88
$System_Drawing_Size.Height = 23
$btnApply.Size = $System_Drawing_Size
$btnApply.UseVisualStyleBackColor = $True

$btnApply.Text = "Apply Changes"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 60
$System_Drawing_Point.Y = 302
$btnApply.Location = $System_Drawing_Point
$btnApply.DataBindings.DefaultDataSourceUpdateMode = 0
$btnApply.add_Click({ApplyChanges_OnClick})

$form.Controls.Add($btnApply)

$label5.TabIndex = 4
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 66
$System_Drawing_Size.Height = 23
$label5.Size = $System_Drawing_Size
$label5.Text = "Owner(s):"
$label5.Font = New-Object System.Drawing.Font("Arial",9,1,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 35
$System_Drawing_Point.Y = 156
$label5.Location = $System_Drawing_Point
$label5.DataBindings.DefaultDataSourceUpdateMode = 0
$label5.Name = "label5"

$form.Controls.Add($label5)

$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 198
$System_Drawing_Size.Height = 20
$txtDLName.Size = $System_Drawing_Size
$txtDLName.DataBindings.DefaultDataSourceUpdateMode = 0
$txtDLName.MaxLength = 255
$txtDLName.Name = "txtDLName"
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 120
$System_Drawing_Point.Y = 48
$txtDLName.Location = $System_Drawing_Point
$txtDLName.TabIndex = 2
$txtDLName.add_KeyDown({if ($_.KeyCode -eq "Enter"){RetrieveDL}})

$form.Controls.Add($txtDLName)

$label4.TabIndex = 1
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 103
$System_Drawing_Size.Height = 23
$label4.Size = $System_Drawing_Size
$label4.Text = "DL Name:  $DLPrefix"
$label4.Font = New-Object System.Drawing.Font("Arial",9,1,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 51
$label4.Location = $System_Drawing_Point
$label4.DataBindings.DefaultDataSourceUpdateMode = 0
$label4.Name = "label4"

$form.Controls.Add($label4)

$label3.TabIndex = 2
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 10
$System_Drawing_Size.Height = 23
$label3.Size = $System_Drawing_Size
$label3.Text = "."

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 138
$System_Drawing_Point.Y = 13
$label3.Location = $System_Drawing_Point
$label3.DataBindings.DefaultDataSourceUpdateMode = 0
$label3.Name = "label3"

$form.Controls.Add($label3)

$toolTip1.ShowAlways = $True
$toolTip1.IsBalloon = $True

$statusBarPanel1.Name = "statusBarPanel1"
$statusBarPanel1.Text = "Connecting to Exchange..."
$statusBarPanel1.AutoSize = 2
$statusBarPanel1.Width = 162

$statusBarPanel2.Name = "statusBarPanel2"
$statusBarPanel2.Text = ""
$statusBarPanel2.AutoSize = 2
$statusBarPanel2.Width = 162
$statusBarPanel2.BorderStyle = 2

$toolTip1.ShowAlways = $True
$toolTip1.IsBalloon = $True
$toolTip1.AutomaticDelay = 250
$toolTip1.AutoPopDelay = 5000
$toolTip1.ToolTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
$txtOwnersTip = "Enter the username of someone to be added as an owner."
$txtDLNameTip = "The display name of the DL as it appears in the address book."
$toolTip1.SetToolTip($txtDLName,$txtDLNameTip)
$toolTip1.SetToolTip($txtAddOwner,$txtOwnersTip)

#endregion Generated Form Code

$OnLoadForm_StateCorrection = {$form.WindowState = $InitialFormWindowState}
$InitialFormWindowState = $form.WindowState
$form.add_Load($OnLoadForm_StateCorrection)
$form.add_Shown({$form.Activate();ConnectToExchange}) #Action when form is displayed
#Show the Form

[void] $form.ShowDialog()
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/script-to-change-security-dl-ownership/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Found a bug with Set-MailboxFolderPermission</title>
		<link>http://www.flobee.net/found-a-bug-with-set-mailboxfolderpermission/</link>
		<comments>http://www.flobee.net/found-a-bug-with-set-mailboxfolderpermission/#comments</comments>
		<pubDate>Thu, 09 Dec 2010 21:39:00 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=527</guid>
		<description><![CDATA[While working with conference room calendars at my company, I set the Default user permission to Reviewer so that anyone can open the calendar to see meetings in a layout that is far easier to work with than the scheduling assistant. If you set the permission with Outlook, everything works well. If you use EMS [...]]]></description>
			<content:encoded><![CDATA[<p>While working with conference room calendars at my company, I set the Default user permission to Reviewer so that anyone can open the calendar to see meetings in a layout that is far easier to work with than the scheduling assistant.  If you set the permission with Outlook, everything works well.  If you use EMS to do it with <span class="flobeecode" id="codekeyword">Set-MailboxFolderPermission</span> (Exchange 2010 SP1), it runs successfully, but not everything works well.  The main thing I have noticed is that users can&#8217;t print the calendar.</p>
<p>I used <a href="http://mfcmapi.codeplex.com/">MAPI Editor</a> (n&eacute;e MFCMAPI) to see what is different when using Outlook versus PowerShell.  The permission set on the Calendar folder is the same with both methods, so I suspected it had something to do with the Freebusy Data folder.  I charted what permission is set on this folder with both methods with different users, and also adding a user and changing an existing one.</p>
<p>What I have found is that both <span class="flobeecode" id="codekeyword">Add-MailboxFolderPermission</span> and <span class="flobeecode" id="codekeyword">Set-MailboxFolderPermission</span> work correctly when applying permissions for named users.  Setting any permission on the calendar results in the person being granted Editor permission to the Freebusy Data folder.  (If you remove the permission on the calendar, the Editor permission remains on the Freebusy Data folder, which isn&#8217;t a problem.)</p>
<p>There is an issue, however, when setting the permission for the Default user entry which, of course, is a special user that simply represents any user who isn&#8217;t explicitly listed in the ACL.  Permission on the Freebusy Data folder is not updated when setting the permission for the Default user entry on the Calendar folder, leaving the entry with a bitmask value of 0 (no rights) as seen in MAPI Editor.</p>
<p>To work around this issue, I looked into using Exchange Web Services and also the EWS Managed API.  I ran into problems enumerating the ACL on the Freebusy Data folder (it is listed as empty).  While trying to resolve that I ended up finding a much simpler solution which doesn&#8217;t need anything more than <span class="flobeecode" id="codekeyword">Set-MailboxFolderPermission</span>.  You can access the Non_IPM_Subtree with the cmdlet just by including it as part of the path.  The workaround is to manually set the permission on the Freebusy Data folder after you do it on Calendar.  This is only necessary when setting the permission for the Default user entry.</p>
<p><span class="flobeecode" id="codekeyword">Set-MailboxFolderPermission</span> <span class="flobeecode" id="codeplain">&#8216;MB Identity:\calendar&#8217;</span> <span class="flobeecode" id="codekeyword">-User</span> <span class="flobeecode" id="codeplain">default</span> <span class="flobeecode" id="codekeyword">-AccessRights</span> <span class="flobeecode" id="codeplain">reviewer</span><br />
<span class="flobeecode" id="codekeyword">Set-MailboxFolderPermission</span> <span class="flobeecode" id="codeplain">&#8216;MB Identity:\non_ipm_subtree\freebusy data&#8217;</span> <span class="flobeecode" id="codekeyword">-User</span> <span class="flobeecode" id="codeplain">default</span> <span class="flobeecode" id="codekeyword">-AccessRights</span> <span class="flobeecode" id="codeplain">reviewer</span></p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/found-a-bug-with-set-mailboxfolderpermission/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>New version of LDAP formatting script</title>
		<link>http://www.flobee.net/new-version-of-ldap-formatting-script/</link>
		<comments>http://www.flobee.net/new-version-of-ldap-formatting-script/#comments</comments>
		<pubDate>Fri, 12 Nov 2010 18:39:32 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=521</guid>
		<description><![CDATA[Over two years ago, I posted a script (in VBScript) that formats an LDAP filter for readability and outputs it in an IE window. I seldom use VBScript anymore since Exchange is all about the PowerShell. I still work with LDAP filters all the time to control membership in automated DLs (Exchange&#8217;s query-based DLs are [...]]]></description>
			<content:encoded><![CDATA[<p>Over two years ago, I <a href="http://www.flobee.net/script-to-format-an-ldap-filter-for-readability/">posted</a> a script (in VBScript) that formats an LDAP filter for readability and outputs it in an IE window.  I seldom use VBScript anymore since Exchange is all about the PowerShell.  I still work with LDAP filters all the time to control membership in automated DLs (Exchange&#8217;s query-based DLs are very limited), so I was working in PowerShell to do that and still hopping over to my old script to format it for myself or others.</p>
<p>I thought it was time to update that script to work in PowerShell.  While some things port from VBScript to PowerShell quite easily, I got stuck in several places doing this one.  Working with the InternetExplorer.Application COM object is one, but researching the changes allowed me to improve the formatting.  The font is smaller and I have added pipes to show where the indenting occurs, which helps with the flow when the filter is long and you have to scroll.</p>
<p>A sticking point was working with the search and replace.  You can&#8217;t use the parameters for the starting position and the number of replacements to make when using the replace operator in PowerShell.  You can, however, do it with the RegularExpressions class (RegEx), but then I had to accommodate the special characters in the regex world.  That was offset by the lack of a need to escape as many characters in PowerShell as in VBScript.</p>
<p>The main issue, though, was interpreting my old code.  It is documented well, but I could not figure out why I was doing certain things to manipulate the filter as the indentations were being added and keeping track of the correct position in the characters.  I spent a fair number of hours trying to figure out if certain lines were just superfluous or were actually needed for it to work.  I was able to remove lines of code that were at least not needed in PowerShell, though I still don&#8217;t know why I used them in them VBScript version.</p>
<p>In the end, the results are the same as the old version except for the formatting changes (which are an improvement).  I have thought about changing the formatted filter to XML so that each indentation can be collapsed when viewing in IE, but that is more work than needed at this time.  The full code is below, but you can also just download the script via the link at the end.  (I have also included the old VBScript version in the download.)</p>
<pre class="brush:ps;auto-links:false">
#Takes an LDAP filter from the clipboard and formats it for readability
#v1.0 11/12/10
#http://www.flobee.net

function GetClipBoard
	{
    Add-Type -AssemblyName System.Windows.Forms
    $tb = New-Object System.Windows.Forms.TextBox
    $tb.Multiline = $true
    $tb.Paste()
    $tb.Text
	}

function FormatLDAPDisplay($infilter)
	{
	#Replace pipes with crosshatches to keep them from interfering in regex
	$infilter = $infilter.Replace('|','#')
	#Iterate through each character in filter and insert line breaks and nesting
	$iPos = 0
	$iIndentCount = 0
	while ($iPos + 1 -lt $infilter.Length)
		{
		$currCharacter = $infilter.Substring($iPos, 1)
		$iWhileIndent = 1
		$sIndentation = ''
		switch -regex ($currCharacter)
			{
			#LDAP operators to watch for to modify nesting level.
			#NOT operator ignored because only used in one-off attribute value
			"[&#]"
				{
				#Operator followed by open paren means nesting increase
				if ($infilter.Substring($iPos + 1, 1) -eq '(')
					{
					$iIndentCount ++
					#Build nest based on number of indentations
					while ($iWhileIndent -le $iIndentCount)
						{
						#Use unique string as placeholder for nesting with HTML spaces
						$sIndentation += '<font color=gray>|</font>QZNBSPQZNBSPQZNBSPQZNBSP'
						$iWhileIndent ++
						}
					#Insert new string for formatting
					$regx = [regex]$currCharacter
					$infilter = $regx.Replace($infilter, "$currCharacter$sIndentation", 1, $iPos)
					#Move current position to next character after inserted string
					$iPos += $sIndentation.Length + 5
					}
				else
					{
					$iPos ++
					}
				}
			"[\(]" #Escape paren to be treated literally in regex
				{
				while ($iWhileIndent -le $iIndentCount)
					{
					$sIndentation += "<font color=gray>|</font>QZNBSPQZNBSPQZNBSPQZNBSP"
					$iWhileIndent ++
					}
				if ($sIndentation -ne '')
					{
					if ($iPos -ne 0)
						{
						#If open paren follows close paren, insert line break
						if ($infilter.Substring($iPos - 1, 1) -eq ')')
							{
							$sIndentation += ''
							$regx = [regex]'\('
							$sNewLDAPFilter = $regx.Replace($infilter, "$sIndentation$currCharacter", 1, $iPos)
							$infilter = $infilter.Substring(0, $iPos - 1) + $sNewLDAPFilter
							$iPos += $sIndentation.Length + 1
							}
						else
							{
							$iPos ++
							}
						}
					else
						{
						$iPos ++
						}
					}
				else
					{
					$iPos ++
					}
				}
			"[\)]" #Escape paren to be treated literally in regex
				{
				#Two consecutive close paren means nesting reduces one level
				if ($infilter.Substring($iPos + 1, 1) -eq ')')
					{
					$iIndentCount --
					$bDblClose = $true
					}
				while ($iWhileIndent -le $iIndentCount)
					{
					$sIndentation += '<font color=gray>|</font>QZNBSPQZNBSPQZNBSPQZNBSP'
					$iWhileIndent ++
					}
				$regx = [regex]'\)'
				$infilter = $regx.Replace($infilter, "$currCharacter$sIndentation", 1, $iPos)
				#Adjust position to account for two close paren
				if ($bDblClose)
					{
					$iPos += $sIndentation.Length + 5
					$bDblClose = $null
					}
				else
					{
					$iPos += $sIndentation.Length + 6
					}
				}
			default
				{
				#No paren or operator means move to next character
				$iPos ++
				}
			}
		}

	#Replace LDAP operators with words
	$infilter = $infilter -replace '&#038;', '<i>AND</i>'
	$infilter = $infilter -replace '#', '<i>OR</i>'
	$infilter = $infilter -replace '!', '<i>NOT </i>'

	#'Replace spaceholders with HTML spaces
	$infilter = $infilter -replace 'QZNBSP', '&nbsp;'

	$infilter
	}

$string = GetClipboard
$htmlbody = FormatLDAPDisplay $string

#Launch IE window
$ie = New-Object -ComObject "InternetExplorer.Application"
$ie.AddressBar = $false
$ie.Menubar = $false
$ie.Toolbar = $false
$ie.Resizable = $true
$ie.Height = 450
$ie.Width = 500
$ie.Visible = $true
$ie.Navigate('about:blank')
do
	{
  	Sleep -Milliseconds 100
	}
while ($ie.Busy)

$doc = $ie.Document
$doc.title = "Formatted LDAP Filter"
$doc.body.style.fontSize = '10pt'
$doc.body.innerHTML = $htmlbody
</pre>
Note: There is a file embedded within this post, please visit this post to download the file.
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/new-version-of-ldap-formatting-script/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Exchange 2010 prerequisite installation script updated</title>
		<link>http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated-2/</link>
		<comments>http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated-2/#comments</comments>
		<pubDate>Tue, 24 Aug 2010 18:37:33 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=515</guid>
		<description><![CDATA[The installation script, originally referenced here, has been updated to version 1.5. It adds support for installing just the management tools, including on Windows 7. I was in the process of making it work on Vista as well, but it got messy because dism doesn&#8217;t come with Vista and I didn&#8217;t want to have separate [...]]]></description>
			<content:encoded><![CDATA[<p>The installation script, originally referenced <a href="http://www.flobee.net/script-to-install-all-exchange-2010-prerequisites/">here</a>, has been updated to version 1.5.  It adds support for installing just the management tools, including on Windows 7.  I was in the process of making it work on Vista as well, but it got messy because dism doesn&#8217;t come with Vista and I didn&#8217;t want to have separate installation routines for Windows 7 and Vista.  So I am making the assumption that anyone who wants to install the management tools on a workstation has long given up on Vista.</p>
<p>To detect the OS (which is actually difficult and not uniform by any means) and make sure it is 64-bit, this sections is added:</p>
<pre class="brush:ps">
# Detect correct OS here and exit if no match
$wmiOS = Get-WMIObject win32_OperatingSystem
$OScap = $wmiOS.Caption
$OSver = $wmiOS.Version
[array]$wmiProc = Get-WmiObject win32_Processor
if ($wmiProc[0].Architecture -eq '9')
	{
	if ($OScap -match 'Windows 7')
		{$os = 'Win7'}
	elseif (($OSver -eq '6.1.7600') -and ($OScap -match '2008'))
		{$os = 'R2'}
	elseif ($OSver -eq '6.0.6002')
		{$os = 'R1'}
	else
...
</pre>
<p>Getting the processor as an array makes it work with both single and multi-processors.  As a reader kindly pointed out, when run on an R2 server, it kept thinking WinRM wasn&#8217;t installed.  This is because WinRM is preinstalled on R2 so the check for the hotfix KB installation will always fail.  It now skips the WinRM check for an R2 server.</p>
<p>I also updated the menu so you can select the management tools, and made it so you only are given the option of selecting the management tools when run on Windows 7.  (Only the menu reflects this restriction.  If you select a different option the script will still try and run that command.)  The download link is below.<br />
Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated-2/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Automatically disable ActiveSync for new mailboxes in Exchange 2010</title>
		<link>http://www.flobee.net/automatically-disable-activesync-for-new-mailboxes-in-exchange-2010/</link>
		<comments>http://www.flobee.net/automatically-disable-activesync-for-new-mailboxes-in-exchange-2010/#comments</comments>
		<pubDate>Wed, 30 Jun 2010 21:51:54 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=510</guid>
		<description><![CDATA[One of the new features in Exchange 2010 is the use of cmdlet extension agents, as described in this post. Using the Scripting Agent you can have Exchange ActiveSync disabled whenever a mailbox is created for a new or existing user. This removes the need to do it directly against Active Directory through some workflow [...]]]></description>
			<content:encoded><![CDATA[<p>One of the new features in Exchange 2010 is the use of cmdlet extension agents, as described in <a href="http://www.ucblogs.net/blogs/exchange/archive/2010/05/29/Using-Scriping-Agent-to-cause-automatic-events-to-occur-in-Exchange-2010-_2D00_-life-just-got-simpler_2100_.aspx">this </a>post.  Using the Scripting Agent you can have Exchange ActiveSync disabled whenever a mailbox is created for a new or existing user.  This removes the need to do it directly against Active Directory through some workflow mechanism or scheduling a task to run that does it with the <span class="flobeecode" id="codekeyword">Set-CASMailbox</span> cmdlet.</p>
<p>There is almost no documentation on the use of the provisioning handler for Exchange 2010, leaving me to do a lot of trial and error to get it working for new mailboxes for both new and existing users.  It doesn&#8217;t look like the provisioning handler has access to any of the information returned by the success of the <span class="flobeecode" id="codekeyword">New-Mailbox</span> and <span class="flobeecode" id="codekeyword">Enable-Mailbox</span> cmdlets.  This means it only has access to the information submitted by the user in a cmdlet.  Because you supply different information when creating a mailbox for a new user compared to an existing one, the code has to be different for each.</p>
<p>Copy the code below into the ScriptingAgentConfig.xml file and, as Pat Richard&#8217;s post details, put it in the CmdletExtensionAgents directory and enable the Scripting Agent.</p>
<pre class="brush:ps;">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
&lt;Configuration version=&quot;1.0&quot;&gt;
	&lt;Feature Name=&quot;MailboxProvisioning&quot; Cmdlets=&quot;enable-mailbox&quot;&gt;
		&lt;ApiCall Name=&quot;OnComplete&quot;&gt;
			if($succeeded)
				{
				$user = (Get-User $provisioningHandler.UserSpecifiedParameters[&quot;Identity&quot;]).distinguishedName
				Set-CASMailbox $user -ActiveSyncEnabled $false
				}
		&lt;/ApiCall&gt;
	&lt;/Feature&gt;
	&lt;Feature Name=&quot;MailboxProvisioning&quot; Cmdlets=&quot;new-mailbox&quot;&gt;
		&lt;ApiCall Name=&quot;OnComplete&quot;&gt;
			if($succeeded)
				{
				$user = (Get-User $provisioningHandler.UserSpecifiedParameters[&quot;Name&quot;]).distinguishedName
				Set-CASMailbox $user -ActiveSyncEnabled $false
				}
		&lt;/ApiCall&gt;
	&lt;/Feature&gt;
&lt;/Configuration&gt;
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/automatically-disable-activesync-for-new-mailboxes-in-exchange-2010/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Exchange 2010 prerequisite installation script updated</title>
		<link>http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated/</link>
		<comments>http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated/#comments</comments>
		<pubDate>Mon, 28 Jun 2010 16:49:38 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=498</guid>
		<description><![CDATA[Edit on August 24, 2010: Script updated to version 1.5. See notes here. I have made a couple of minor updates to the prerequisite installation script. One is changing the way the source directory is determined. Instead of hardcoding the directory, the directory that the script is running from will be used. This allows you [...]]]></description>
			<content:encoded><![CDATA[<p><font color="green">Edit on August 24, 2010: Script updated to version 1.5.  See notes <a href="http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated-2/">here</a>.</font></p>
<p>I have made a couple of minor updates to the prerequisite installation script.  One is changing the way the source directory is determined.  Instead of hardcoding the directory, the directory that the script is running from will be used.  This allows you to simply put all files in the same directory as the script and run it from anywhere (except for UNC).</p>
<p>Another change is a correction when checking for installation binaries.  I failed to account for the backslash between the directory and the filename so, even when the file exists, the script wouldn&#8217;t see it.</p>
<p>The last change is moving the NETTCPPortSharing service setting after the Windows components installation completes so that the setting is immediately configured.  This allows you to begin the Exchange installation without rebooting.</p>
<p>The download and inline code from the <a href="http://www.flobee.net/script-to-install-all-exchange-2010-prerequisites/">original post</a> have been updated.<br />
Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Script to install all Exchange 2010 prerequisites</title>
		<link>http://www.flobee.net/script-to-install-all-exchange-2010-prerequisites/</link>
		<comments>http://www.flobee.net/script-to-install-all-exchange-2010-prerequisites/#comments</comments>
		<pubDate>Thu, 10 Jun 2010 20:55:43 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=491</guid>
		<description><![CDATA[Edit on August 24, 2010: Script updated to version 1.5. See notes here. There are already several sites which have scripts to install Exchange 2010 prerequisites, but they all seem to fall short in one aspect or another. Some make you choose the right script if you are on 2008 or 2008 R2. Others download [...]]]></description>
			<content:encoded><![CDATA[<p><font color="green">Edit on August 24, 2010: Script updated to version 1.5.  See notes <a href="http://www.flobee.net/exchange-2010-prerequisite-installation-script-updated-2/">here</a>.</font></p>
<p>There are already several sites which have scripts to install Exchange 2010 prerequisites, but they all seem to fall short in one aspect or another.  Some make you choose the right script if you are on 2008 or 2008 R2.  Others download some files but not others.  None make sure that WinRM is installed, which is needed for PowerShell v2.  So I started with a great script and made it better.  My starting point is <a href="http://www.bhargavs.com/index.php/2009/11/18/script-to-install-exchange-2010-pre-requisites-for-windows-server-2008-r2/">Bhargav Shukla&#8217;s script</a>, which in turn started elsewhere.</p>
<p>My script checks whether the server is running Windows Server 2008 SP2 or Windows Server 2008 R2.  (If you aren&#8217;t running SP2, it will tell you.)  Because the command to add the Windows components is different between them (ServerManagerCmd.exe in DOS and Add-WindowsFeature in PowerShell), the appropriate command will be run.</p>
<p>Instead of just downloading the 2007 Office Filter Pack, I added the option to download each file (including WinRM) if it isn&#8217;t installed and not in the installation source directory.  I also chose to install the 2010 Office Filter Pack, which includes support for 2007.</p>
<p>If Windows Remote Management isn&#8217;t installed, it will install it and reboot the server.  You don&#8217;t have to uninstall PowerShell v1 to do this, so you can run the script and install WinRM, then run the script again after the reboot to continue (which is then implicitly using PowerShell v2).</p>
<p>I also added functions to enable remote PowerShell and enable the Windows Firewall service.  The latter is required for Exchange setup to be successful so it can create the firewall exceptions.  You can always disable the service after setup is complete.</p>
<p>I added the option to install the HT and CAS roles together, should that apply to you.</p>
<p>If you want to download the required files ahead of time and put them in the same directory as the script, they are listed below (direct download links).  Note that I renamed the filter pack installation file to 2010FilterPack64bit.exe in order to distinguish it from the Office 2007 Filter Pack installation file.  If you have the 2007 Filter pack installed, the 2010 install will replace it upon installation.  If you manually download the filter pack, though, remember to rename it.</p>
<ul>
<li><a href="http://download.microsoft.com/download/2/8/6/28686477-3242-4E96-9009-30B16BED89AF/Windows6.0-KB968930-x64.msu">Windows Remote Management Framework</a> (14MB)</li>
<li><a href="http://download.microsoft.com/download/2/0/e/20e90413-712f-438c-988e-fdaa79a8ac3d/dotnetfx35.exe">.NET 3.5 SP1 Redistributable installation</a> (235MB)</li>
<li><a href="http://download.microsoft.com/download/B/4/2/B42197BD-AEE1-4FE6-8CB3-29D60D0C3727/Windows6.0-KB958483-x64.msu">.NET 3.5 SP1 hotfix</a> (1.4MB)</li>
<li><a href="http://download.microsoft.com/download/0/A/2/0A28BBFA-CBFA-4C03-A739-30CCA5E21659/FilterPack64bit.exe">Office 2010/2007 Filter Pack</a> (4MB)</li>
<li>The Exchange-<i>Role</i>.xml files from the installation media&#8217;s Scripts directory</li>
</ul>
<p>You can copy the script inline below, or download it via the link at the bottom of the post.</p>
<pre class="brush: ps">
#Installs prerequisites necessary to install Exchange 2010 on
#Windows 2008 SP2 or Windows 2008 R2.
#Version 1.3
#Last modified: June 28, 2010

#Set installation source to same directory as script execution
$sourcePath = Split-Path -Parent $MyInvocation.MyCommand.Path

Write-Host 'Using ' -NoNewline
Write-Host $sourcePath -ForegroundColor DarkGreen -NoNewline
Write-Host ' as the installation source.'

# Detect correct OS here and exit if no match
if ((Get-WMIObject win32_OperatingSystem).Version -eq '6.1.7600')
	{$os = 'R2'}
elseif ((Get-WMIObject win32_OperatingSystem).Version -eq '6.0.6002')
	{$os = 'R1'}
else
	{
	Write-Host 'This script requires Windows Server 2008 with SP2, or R2, which this is not.' -ForegroundColor Red -BackgroundColor Black
	break
	}

#Installation files and properties (filename, shortname, displayname, download URL, size)
$fileWinRM = ('Windows6.0-KB968930-x64.msu','WinRM','Windows Remote Management Framework','http://download.microsoft.com/download/2/8/6/28686477-3242-4E96-9009-30B16BED89AF/Windows6.0-KB968930-x64.msu','14MB')
$fileNET35 = ('dotnetfx35.exe','.NET 3.5','.NET 3.5 SP1','http://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe','235MB')
$fileNET35HF = ('NDP35SP1-KB958484-x64.exe','.NET 3.5 hotfix','.NET 3.5 hotfix','http://download.microsoft.com/download/B/4/2/B42197BD-AEE1-4FE6-8CB3-29D60D0C3727/Windows6.0-KB958483-x64.msu','1.4MB')
$fileOFP = ('2010FilterPack64bit.exe','Office 2010 Filter Pack','Office 2010/2007 Filter Pack','http://download.microsoft.com/download/0/A/2/0A28BBFA-CBFA-4C03-A739-30CCA5E21659/FilterPack64bit.exe','4MB')

Function InstallApp($app)
	{
	switch ($app)
		{
		'WinRM'
			{
			$appArray = $fileWinRM
			$kb = 'KB968930'
			}
		'NET35'
			{
			$appArray = $fileNET35
			$checkExpression = "test-path 'HKLM:Software\Microsoft\NET Framework Setup\NDP\v3.5'"
			}
		'NET35HF'
			{
			$appArray = $fileNET35HF
			$checkExpression = "test-path 'HKLM:SOFTWARE\Wow6432Node\Microsoft\Updates\Microsoft .NET Framework 3.5 SP1\SP1\KB958484'"
			}
		'OFP'
			{
			$appArray = $fileOFP
			$checkExpression = "test-path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\{95140000-2000-0409-1000-0000000FF1CE}'"
			}
		}
	trap
		{
		Write-Host ''
		Write-Host "There was a problem downloading or installing $($appArray[1])." -ForegroundColor Red
		Write-Host ''
		break
		}
	#Check for existing installation
	Write-Host "Verifying $($appArray[2]) is installed..." -NoNewline
	if ($app -eq 'WinRM')
		{
		$hfInst = Get-WMIObject Win32_QuickFixEngineering | where {$_.HotFixID -eq $kb}
		if ($hfInst)
			{
			$bInstalled = $true
			}
		else
			{
			$bInstalled = $false
			}
		}
	else
		{
		if (Invoke-Expression $checkExpression)
			{
			$bInstalled = $true
			}
		else
			{
			$bInstalled = $false
			}
		}
	if ($bInstalled)
		{
		Write-Host "$($appArray[2]) is installed." -ForegroundColor Green
		return
		}
	Write-Host "$($appArray[2]) is not installed." -ForegroundColor Red
	Write-Host "Installing $($appArray[2])..." -NoNewline

	#Install app:  Check for existing installation file.
	$fullPath = $sourcePath+"\$($appArray[0])"
	if (!(Test-Path $fullPath))
		{
		Write-Host ''
		Write-Host "$($appArray[0]) not found in source path." -ForegroundColor Yellow
		$dl = Read-Host "Do you want to download it now? ($($appArray[4]))(Y/N)"
		if ($dl -ne 'y')
			{
			Write-Host "You have chosen to not download the $($appArray[1]) installation file."
			Write-Host "Put $($appArray[0]) in the source directory and run the script again."
			break
			}
		else
			{
			Write-Host "Downloading $($appArray[1])..." -NoNewline
			$dlClient = New-Object System.Net.WebClient
			$dlClient.DownloadFile($appArray[3],$fullPath)
			if (!(Test-Path $fullPath))
				{
				Write-Host ''
				Write-Host "There was a problem downloading $($appArray[1])." -ForegroundColor Red
				Write-Host ''
				}
			else
				{
				Write-Host 'done.' -ForegroundColor Green
				}
			}
		}

	#Install app: Run installation.
	if ($app -eq 'WinRM')
		{
		$expression = "wusa $fullPath /quiet"
		Invoke-Expression $expression
		Write-Host 'External update process started...Be patient, it takes time.' -ForegroundColor Yellow
		Write-Host ''
		Write-Host 'When the WinRM installation is complete, the system will automatically reboot.'
		Write-Host 'Then you can rerun the script to continue.  This script will now end.'
		break
		}
	else
		{
		if ($app -eq 'NET35HF')
			{$arguments = '/passive /norestart'}
		else
			{$arguments = '/quiet /norestart'}
		$process = [System.Diagnostics.Process]::Start($fullPath,$arguments)
		$process.WaitForExit()
		Write-Host "$($appArray[1]) installation complete." -ForegroundColor Green
		}
	}	

Function InstallNET35()
	{
	InstallApp('NET35')
	InstallApp('NET35HF')
	}

Function SetTCPSharing()
	{
	trap
		{
		Write-Host ''
		Write-Host 'There was problem setting the NET TCP Port Sharing service to Automatic startup.' -ForegroundColor Red
		Write-Host 'The service must be set to Automatic for Exchange setup to be successful.' -ForegroundColor Red
		Write-Host ''
		return
		}
	#Set NETTCPPortSharing to Automatic
	Write-Host 'Configuring the NET TCP Port Sharing service...' -NoNewline
	Set-Service NetTcpPortSharing -StartupType Automatic
	Write-Host 'done.' -ForegroundColor Green
	}

Function EnableRemoting()
	{
	trap
		{
		Write-Host ''
		Write-Host 'There was problem configuring the system for remote PowerShell.' -ForegroundColor Red
		Write-Host ''
		return
		}
	#Enable Remote PowerShell for Exchange administration from workstations
	Write-Host 'Enabling system for remote PowerShell connections...'
	Enable-PSRemoting -force
	Write-Host 'Remote PowerShell configuration is done.' -ForegroundColor Green
	}

Function EnableFirewall()
	{
	trap
		{
		Write-Host ''
		Write-Host 'There was problem starting the Windows Firewall service.' -ForegroundColor Red
		Write-Host 'The firewall service must be running during Exchange setup.  It can be stopped after it completes.' -ForegroundColor Red
		Write-Host ''
		return
		}
	#Ensure Windows Firewall is running or Exchange install will fail
	Write-Host 'Starting the Windows Firewall service...' -NoNewline
	Set-Service 'MpsSvc' -StartupType Automatic -Status Running
	Write-Host 'done.' -ForegroundColor Green
	}

if ($os -eq 'R1')
	{
	$ht = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-Hub.xml'
	$cas = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-CAS.xml'
	$mbx = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-MBX.xml'
	$um = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-UM.xml'
	$edge = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-Edge.xml'
	$typical = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-Typical.xml'
	}
elseif ($os -eq 'R2')
	{
	$ht = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server -restart'
	$cas = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server,Web-ISAPI-Ext,Web-Digest-Auth,Web-Dyn-Compression,NET-HTTP-Activation,RPC-Over-HTTP-Proxy -restart'
	$mbx = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server -restart'
	$um = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server,Desktop-Experience -restart'
	$edge = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,ADLDS -restart'
	$typical = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server,Web-ISAPI-Ext,Web-Digest-Auth,Web-Dyn-Compression,NET-HTTP-Activation,RPC-Over-HTTP-Proxy -restart'
	Import-Module ServerManager
	}
$opt = 'None'

InstallApp('WinRM')

clear
if ($opt -ne 'None') {write-host 'Last command: '$opt -foregroundcolor Yellow}
write-host
write-host 'Exchange Server 2010 Prerequisites Installation'
write-host 'Please select which role you are going to install:'
write-host
write-host '1)  Hub Transport'
write-host '2)  Client Access Server'
write-host '3)  Mailbox'
write-host '4)  Unified Messaging'
write-host '5)  Edge Transport'
write-host '6)  Typical (CAS\HT\Mailbox)'
write-host '7)  Client Access and Hub Transport'
write-host
write-host '9)  Configure NetTCP Port Sharing service'
write-host '    Required for the Client Access Server role' -foregroundcolor yellow
write-host '    Automatically set for options 2,6, and 7' -foregroundcolor yellow
write-host '10) Install 2010 Office System Converter: Microsoft Filter Pack'
write-host '    Required if installing Hub Transport or Mailbox Server roles' -foregroundcolor yellow
write-host '    Automatically set for options 1,3,6, and 7' -foregroundcolor yellow
Write-Host '11) Enable PowerShell Remoting'
Write-Host '    Automatically set for options 1,2,3,4,6, and 7' -ForegroundColor Yellow
write-host
write-host '13) Restart the Server'
write-host '14) End'
write-host
Write-Host 'Note: Using ' -NoNewline
Write-Host $sourcePath -ForegroundColor DarkGreen -NoNewline
Write-Host ' as the installation source.'
$opt = Read-Host 'Select an option.. [1-14]? '

switch ($opt)
	{
	1{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $ht
	  	}
	2{
		InstallNET35; EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $cas
        SetTCPSharing
		}
	3{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $mbx
		}
	4{
		InstallNET35; EnableRemoting; EnableFirewall
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $um
		}
	5{
		InstallNET35; EnableFirewall
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $edge
		}
	6{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $typical
        SetTCPSharing
		}
	7{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $cas
        SetTCPSharing
		}
	9 { SetTCPSharing }
	10 { InstallApp('OFP') }
	11 { EnableRemoting }
	13 { Restart-Computer }
	14 {write-host 'Exiting...'}
	default {write-host "You haven't selected any of the available options."}
	}
</pre>
Note: There is a file embedded within this post, please visit this post to download the file.
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/script-to-install-all-exchange-2010-prerequisites/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Disable Exchange ActiveSync for users on Exchange 2010</title>
		<link>http://www.flobee.net/disable-exchange-activesync-for-users-on-exchange-2010/</link>
		<comments>http://www.flobee.net/disable-exchange-activesync-for-users-on-exchange-2010/#comments</comments>
		<pubDate>Wed, 12 May 2010 18:09:46 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2010]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=486</guid>
		<description><![CDATA[As with all previous versions of Exchange, the default permission in Exchange 2010 for user accounts is to allow Exchange ActiveSync. I don&#8217;t like this option and am surprised that Microsoft still doesn&#8217;t provide the ability to reverse this. Policy at my company requires you to sign a waiver acknowledging that we, as IT at [...]]]></description>
			<content:encoded><![CDATA[<p>As with all previous versions of Exchange, the default permission in Exchange 2010 for user accounts is to allow Exchange ActiveSync.  I don&#8217;t like this option and am surprised that Microsoft still doesn&#8217;t provide the ability to reverse this.  Policy at my company requires you to sign a waiver acknowledging that we, as IT at the company, have the right to wipe your phone and any personal data that may be on it.</p>
<p>The AD attribute for controlling access to Exchange ActiveSync is still msExchOMAAdminWirelessEnable.  This attribute has been around since Exchange 2003, controlling access to Outlook Mobile Access (OMA) and whether Automatic Up-to-Date Notifications (AUTD), the precursor to Direct Push, was enabled.  Since then, OMA and AUTD have both been discontinued, and you can&#8217;t even disable Direct Push anymore in 2010.  (I don&#8217;t know why you would do so anyway.  I assume it was to control high data charges before everyone started using unlimited data plans.)</p>
<p>So the only value in the attribute that has any effect is 4, which disables Exchange ActiveSync.  Any other value, including &lt;not set&gt;, has no impact, thereby allowing the user to sync.  I have a reason for wanting to set the attribute directly in AD, but you can accomplish the same thing in the Exchange Management Shell by using <span class="flobeecode" id="codekeyword">Set-CASMailbox</span> <span class="flobeecode" id="codeplain">user</span> <span class="flobeecode" id="codekeyword">-ActiveSyncEnabled</span><span class="flobeecode" id="codeplain">:</span><span class="flobeecode" id="codevariable">$false</span>.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/disable-exchange-activesync-for-users-on-exchange-2010/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Determine the proxy server when automatic detection is used</title>
		<link>http://www.flobee.net/determine-the-proxy-server-when-automatic-detection-is-used/</link>
		<comments>http://www.flobee.net/determine-the-proxy-server-when-automatic-detection-is-used/#comments</comments>
		<pubDate>Thu, 25 Mar 2010 16:09:58 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=476</guid>
		<description><![CDATA[If a browser is configured to detect proxy server settings automatically, the proxy server address is never displayed anywhere in the browser. You may have an application, however, that requires you to explicitly define the proxy server to use. To determine the server to use, you can use PowerShell. (New-Object System.Net.WebClient).Proxy.GetProxy("http://www.cnn.com").Authority The URL to use [...]]]></description>
			<content:encoded><![CDATA[<p>If a browser is configured to detect proxy server settings automatically, the proxy server address is never displayed anywhere in the browser.  You may have an application, however, that requires you to explicitly define the proxy server to use.  To determine the server to use, you can use PowerShell.</p>
<pre class="brush:ps;collapse:false;light:true;auto-links:false">
(New-Object System.Net.WebClient).Proxy.GetProxy("http://www.cnn.com").Authority
</pre>
<p>The URL to use in the query can be anything as long as it is one that will evaluate to using a proxy server, so don&#8217;t use an internal address that will evaluate to connecting directly.  The query will return the DNS name of the proxy server and the port to use, e.g., proxy.company.com:8080.  If you want these values separately, you can use the Host and Port properties.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/determine-the-proxy-server-when-automatic-detection-is-used/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Update to OCS archiving script</title>
		<link>http://www.flobee.net/update-to-ocs-archiving-script/</link>
		<comments>http://www.flobee.net/update-to-ocs-archiving-script/#comments</comments>
		<pubDate>Mon, 01 Mar 2010 18:29:51 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[OCS 2007]]></category>
		<category><![CDATA[OCS 2007 R2]]></category>
		<category><![CDATA[PowerShell]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=471</guid>
		<description><![CDATA[There are a couple of changes that have been made to improve the script. First, after running a report for a user with thousands of messages and not having any idea of the progress while it was running, I added progress windows during the two phases (content conversion and HTML report creation). The progress window [...]]]></description>
			<content:encoded><![CDATA[<p>There are a couple of changes that have been made to improve the script.</p>
<p>First, after running a report for a user with thousands of messages and not having any idea of the progress while it was running, I added progress windows during the two phases (content conversion and HTML report creation).  The progress window will tell you how many messages will be processed and how far along it is in that process.</p>
<p>Second, I discovered that hyperlinks in messages were not showing up in the report.  Looking at the HTML (or XML) source, I could see the link was there, but the way it was formatted in the XML file (using element enclosures) meant the conversion to HTML was confusing the parser.  Now I remove those characters prior to the link being written to the XML file.</p>
<p>I also optimized the message body conversion loop to remove redundant or obsolete code that was in the original script.  There are also some minor cosmetic changes here and there.</p>
<p>Lastly, I realized last week that the script only reports on peer-to-peer (P2P) conversations, not multiparty.  I spent some time trying to get multiparty conversations out of the database, but it is proving much more difficult than I anticipated.  The format of multiparty IMs in the archiving database is very different than P2P conversations.  I am still working on it, but I am not sure I will be able to make it work.  In the meantime, I added a note in the report that states it is only for P2P conversations.</p>
<p>The original zip file with the script has been updated, but you can download it here:<br />
Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/update-to-ocs-archiving-script/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Script to compare deployed BlackBerry handheld firmware with a local repository</title>
		<link>http://www.flobee.net/script-to-compare-deployed-blackberry-handheld-firmware-with-a-local-repository/</link>
		<comments>http://www.flobee.net/script-to-compare-deployed-blackberry-handheld-firmware-with-a-local-repository/#comments</comments>
		<pubDate>Tue, 09 Feb 2010 19:08:07 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[BlackBerry]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=463</guid>
		<description><![CDATA[If you use, say, BlackBerry Web Desktop Manager to facilitate firmware upgrades for users, then you have a bunch of them installed somewhere. (I have 49 versions among 15 carriers.) I keep a list of the ones I have installed, but I wanted to be able to compare that to what is actually in use [...]]]></description>
			<content:encoded><![CDATA[<p>If you use, say, BlackBerry Web Desktop Manager to facilitate firmware upgrades for users, then you have a bunch of them installed somewhere.  (I have 49 versions among 15 carriers.)  I keep a list of the ones I have installed, but I wanted to be able to compare that to what is actually in use among all the users in the organization.  This way I can know which installs are obsolete, when new models are in use, and when newer versions are in use by users.</p>
<p>To use this script, in BlackBerry Manager (I am using 4.1.6) highlight all of your users and right-click on them, then select Export Asset Summary Data.  The script will look for BESHandhelds.txt, but you can edit it to be whatever you want.  The file that I use to track installed versions is just a csv with the following headers: Carrier, Model, Model Name, Version, Installed.  Model Name is not used in the script, but mirrors the fields I use in an internal KB article that users can reference.  Here is a sample table of entries:</p>
<table align="center" border="1">
<tbody>
<tr>
<td>Carrier</td>
<td>Model</td>
<td>Model Name</td>
<td>Version</td>
<td>Installed</td>
</tr>
<tr>
<td>AT&amp;T</td>
<td>8320</td>
<td>Curve</td>
<td>4.5.0.182</td>
<td></td>
</tr>
<tr>
<td>AT&amp;T</td>
<td>8520</td>
<td>Curve</td>
<td>4.6.1.314</td>
<td>No</td>
</tr>
<tr>
<td>KPN</td>
<td>8820</td>
<td></td>
<td>4.5.0.55</td>
<td></td>
</tr>
</tbody>
</table>
<p>I added the Installed field as a way to track models that don&#8217;t actually have a local installation either because there is only one release from the carrier or all handhelds are already on that version so there isn&#8217;t a reason to have it installed.  The script will look for a file called BBFirmware.csv but, again, you can change it to whatever you want.</p>
<p>The <span class="flobeecode" id="codekeyword">switch</span> statement starting in line 17 is to map the carrier name as recorded in BES to how I record it in my file.  The output of the script will let you know carriers that are not defined in the statement, but you will want to add or remove carriers as necessary first.</p>
<p>The output will also let you know if a model is in use that is not found in your installation file, if a there is a newer version for a particular model that is in use, and if all users are using the latest version you have installed.  What this script does <i>not</i> do is determine if you have the latest version that has been released by the carrier (since that would be much harder to do).  Lastly, it will let you know of installations you have for models that are not in use anymore so that you can uninstall them.</p>
<p>I am a perfectionist, so I can already see ways to improve the script, but I would never be done if I didn&#8217;t stop somewhere.  You can download the ps1 file below.<br />
Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/script-to-compare-deployed-blackberry-handheld-firmware-with-a-local-repository/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Rio Receiver software can&#8217;t read Unicode ID3 tags</title>
		<link>http://www.flobee.net/rio-receiver-software-cant-read-unicode-id3-tags/</link>
		<comments>http://www.flobee.net/rio-receiver-software-cant-read-unicode-id3-tags/#comments</comments>
		<pubDate>Tue, 26 Jan 2010 01:16:38 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=458</guid>
		<description><![CDATA[I have four Rio Receivers, which use Rio&#8217;s Audio Receiver Manager software on a host PC to serve music. Over the years, certain tracks wouldn&#8217;t be listed in the database. I had looked at the encoder used, what ID3 tags were present, coexistence of v1 and v2.x tags, filenames. But I couldn&#8217;t figure it out. [...]]]></description>
			<content:encoded><![CDATA[<p>I have four Rio Receivers, which use Rio&#8217;s Audio Receiver Manager software on a host PC to serve music.  Over the years, certain tracks wouldn&#8217;t be listed in the database.  I had looked at the encoder used, what ID3 tags were present, coexistence of v1 and v2.x tags, filenames.  But I couldn&#8217;t figure it out.</p>
<p>I have since tried several of the alternate ARM programs out there: <a href="http://www.myhap.org.uk/medianet.htm">MediaNet</a>, <a href="http://jreceiver.sourceforge.net/">JReceiver</a>, <a href="http://sourceforge.net/projects/rioplay/">rioplay</a>, and there may have been one or two others.  But I could never get any of them to work.  I got close recently with JReceiver, but instructions that date back to 2003 do not account for today&#8217;s versions of the various components.</p>
<p>I decided to take another look at why some tracks don&#8217;t show up in the database.  Forum postings indicate it should have no problem with ID3v2.x.  All of my tracks have v2.3 and no v1.x tags.  And most tracks are indexed fine.  So I started reading about the history of ID3, the details of the different versions, and notable incompatibilities such as Windows Media Player 12 still won&#8217;t read v2.4 tags.</p>
<p>The text encoding available in ID3v2 made me think because I know of some programs can&#8217;t read a Unicode text file; it must stored in ANSI.  So referred to my tag editing program, <a href="http://www.softpointer.com/tr.htm">Tag &amp; Rename</a>, and there is a setting for enabling the writing of v2 tags in Unicode.  But if a tag is in Unicode format, T&amp;R doesn&#8217;t indicate this; only when a tag is rewritten will it use the appropriate format based on the setting.</p>
<p>I used a different tagging application, <a href="http://kid3.sourceforge.net/">Kid3</a>, to show which tags are in what format (down to individual tags in a track).  I tested with a track and encoded all relevant fields (artist, title, album) in Unicode.  When I indexed it in ARM, it didn&#8217;t show up.  I used Kid3 to change the tags to ISO-8859-1, indexed again, and voila!  I did further testing and determine that if you format, say, title in Unicode but the artist in ISO-8859-1, the artist will show up in the database, but not the song underneath the artist.  So ARM still reads all the tags in a track; it is just those that are encoded in Unicode are not added to the database (the individual tags).</p>
<p>Since I am not trying to use international characters sets there is nothing really to be gained by storing in Unicode.  I have asked the developer of T&amp;R to add the ability to see what format a given tag is in (similar to Kid3), but in the meantime I have been rewriting my tags without the Unicode setting enabled as I check for album art in my tags.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/rio-receiver-software-cant-read-unicode-id3-tags/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>OCS Archiving Reporter: Group by conversation, filter dates</title>
		<link>http://www.flobee.net/ocs-archiving-reporter-group-by-conversation-filter-dates/</link>
		<comments>http://www.flobee.net/ocs-archiving-reporter-group-by-conversation-filter-dates/#comments</comments>
		<pubDate>Fri, 04 Dec 2009 00:46:21 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[OCS 2007]]></category>
		<category><![CDATA[OCS 2007 R2]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=433</guid>
		<description><![CDATA[Edit: The inline code is not the latest version of the script. To get the latest version, download the full script on the Downloads page (or via the link at the end of the post). The foundation for my script is from the OCS team at Microsoft, who created the original version. The script from [...]]]></description>
			<content:encoded><![CDATA[<p><font color="red">Edit:</font>  The inline code is not the latest version of the script.  To get the latest version, download the full script on the <a href="http://www.flobee.net/downloads">Downloads</a> page (or via the link at the end of the post).</p>
<p>The foundation for my script is from the OCS team at Microsoft, who created the <a href="http://blogs.technet.com/b/nexthop/archive/2009/09/29/updated-powershell-script-for-retrieving-im-from-archiving-database.aspx">original version</a>.</p>
<p>The script from Microsoft grabs all the messages and outputs them all in one big table.  I wanted it to be displayed more logically, grouping an entire conversation (session) together so it is easier to follow the context of the messages.  This proved to be more daunting than I had anticipated.</p>
<p>All messages that occur in the same session have the same value for the SessionIdTime.  With that I can group those messages together.  But I needed a way to know when a new conversation started and ended. To get that I used the RANK function in SQL so that any rank of 1 indicates a new conversation.  The resulting SQL query for a single user is the following (the ticks at the end of the lines are for PowerShell):</p>
<pre class="brush:sql;gutter:false">
SELECT RANK() OVER(PARTITION BY SessionIdTime ORDER BY SessionIdTime, MessageIdTime) AS 'Rank', `
	SessionIdTime, MessageIdTime, Body, ContentTypeId, [from], [to] `
FROM `
(SELECT SessionIdTime, MessageIdTime, Body, ContentTypeId, u1.UserUri AS [from], u2.UserUri AS [to] FROM Messages, `
	Users u1, Users u2 WHERE Messages.FromId = u1.UserId AND Messages.ToId = u2.UserId AND u1.UserUri = '$User1' `
	AND LcsLog.dbo.Messages.Toast IS NULL `
UNION ALL `
SELECT SessionIdTime, MessageIdTime, Body, ContentTypeId, u2.UserUri AS [from], u1.UserUri AS [to] FROM Messages, `
	Users u2, Users u1 WHERE Messages.FromId = u2.UserId AND Messages.ToId = u1.UserId AND u1.UserUri = '$User1' `
	AND LcsLog.dbo.Messages.Toast IS NULL `
)AS dConversation
</pre>
<p>Now that I had the data, I needed to be able to use the XSLT to conditionally close a table the next time a rank of 1 is in the loop.  I had the hardest time trying to make the xsl:if and xsl:choose statements work.  Trying to include a <span class="flobeecode" id="codekeyword">&lt;/table&gt;</span> tag without the loop seeing the opening tag only caused errors.  If I used HTML notation, <span class="flobeecode" id="codekeyword">&amp;lt;/table&amp;gt;</span>, those characters were rendered as their literal ASCII representations rather than being parsed as HTML tags.</p>
<p>After trying for many, many hours, I decided to give up relying on the XML parser to convert the file to HTML and just do that parsing in the PowerShell script.  This has the benefit of not requiring external files to run the script (the XSLT and msxsl.exe).  The code below takes the XML output from the first part of the script and loops through each node (message).  If the rank is 1, it closes the previous table, opens a new one, and writes the message to a row.  If the rank is not 1, it means that a conversation has already been started and the message can simply being written in a new row.</p>
<pre class="brush:ps">
#Convert XML to HTML
$sourceXML = [xml](Get-Content $LocalPath)
Remove-Item -path IM.html -ea SilentlyContinue
pwd | % {[string]$LocalPath = $_.path}
$LocalPath = $LocalPath + "\IM.html"
Add-Content -path $LocalPath -Value '&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"'
Add-Content -Path $LocalPath -Value '  "http://www.w3.org/TR/html4/loose.dtd"&gt;'
Add-Content -Path $LocalPath -Value "&lt;html&gt;"
Add-Content -Path $LocalPath -Value "&lt;head&gt;"
Add-Content -Path $LocalPath -Value "&lt;title&gt;IM Conversation Archive&lt;/title&gt;"
Add-Content -Path $LocalPath -Value "&lt;/head&gt;"
Add-Content -Path $LocalPath -Value "&lt;body&gt;"

Add-Content -Path $LocalPath -Value '&lt;font size="2" face="Verdana"&gt;'
Add-Content -Path $LocalPath -Value "&lt;h2 align=`"center`"&gt;$($sourceXML.IMConversation.Title)&lt;/h2&gt;"
Add-Content -Path $LocalPath -Value "&lt;h3 align=`"center`"&gt;$($sourceXML.IMConversation.Subtitle)&lt;/h2&gt;"
Add-Content -Path $LocalPath -Value "&lt;/font&gt;"
Add-Content -Path $LocalPath -Value '&lt;table border="0" cellpadding="1" width="100%" style="FONT-SIZE:8pt;FONT-FAMILY:verdana"&gt;'
foreach($IM in $sourceXML.IMConversation.IM)
	{
	if ($IM.Rank -eq "1")
		{
		Add-Content -Path $LocalPath -Value "&lt;/tbody&gt;&lt;/table&gt;&lt;br&gt;"
		Add-Content -Path $LocalPath -Value '&lt;table border="1" cellpadding="1" width="100%" style="FONT-SIZE:8pt;FONT-FAMILY:verdana">'
		Add-Content -Path $LocalPath -Value '&lt;thead>&lt;tr bgcolor="#C0C0C0" align="center"&gt;'
		Add-Content -Path $LocalPath -Value	'&lt;th&gt;&lt;font color="#0000FF"&gt;Session Time (UTC):&lt;/font&gt;&lt;/th&gt;'
		Add-Content -Path $LocalPath -Value	'&lt;th&gt;&lt;font color="#0000FF"&gt;Message Time (UTC):&lt;/font&gt;&lt;/th&gt;'
		Add-Content -Path $LocalPath -Value	'&lt;th&gt;&lt;font color="#0000FF"&gt;From:&lt;/font&gt;&lt;/th&gt;'
		Add-Content -Path $LocalPath -Value	'&lt;th&gt;&lt;font color="#0000FF"&gt;To:&lt;/font&gt;&lt;/th&gt;'
		Add-Content -Path $LocalPath -Value '&lt;th width="40%"&gt;&lt;font color="#0000FF"&gt;Message:&lt;/font&gt;&lt;/th&gt;'
		Add-Content -Path $LocalPath -Value	"&lt;/tr&gt;&lt;/thead&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;tbody&gt;&lt;tr&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td rowspan=`"100`" valign=`"top`"&gt;$($IM.SessionTime)&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.DateTime)&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.From)&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.To)&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.Body.get_FirstChild().get_Data())&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;/tr&gt;"
		}
	else
		{
		Add-Content -Path $LocalPath -Value	"&lt;tr&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.DateTime)&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.From)&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.To)&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;td&gt;$($IM.Body.get_FirstChild().get_Data())&lt;/td&gt;"
		Add-Content -Path $LocalPath -Value	"&lt;/tr&gt;"
		}
	}

Add-Content -Path $LocalPath -Value "&lt;/table>"
Add-Content -Path $LocalPath -Value "&lt;/body>"
Add-Content -Path $LocalPath -Value "&lt;/html>"
</pre>
<p>Lastly, I wanted to be able to filter on a date range within the archiving database.  So I added two arguments to the script after the users: <span class="flobeecode" id="codekeyword">-start</span> and <span class="flobeecode" id="codekeyword">-end</span>, both which are independently optional.  If you don&#8217;t specify a second user or you only specify an end date with one or two users, you need to use the parameter names in the command.  Otherwise, they are positional.  The date format to use is <span class="flobeecode" id="codestring">&quot;YYYY-MM-DD&quot;</span>.  To account for the presence of the date filter, this code is added to the script:</p>
<pre class="brush:ps">
If ($start -ne '')
	{
	#Start date has been specified
	$startdate = $start
	$start = "and Messages.MessageIdTime &gt;= '$start' "
	}
Else
	{
	$startdate = "Oldest record in the database"
	}

If ($end -ne '')
	{
	#End date has been specified
	$enddate = $end
	$end = "and Messages.MessageIdTime &lt;= '$end' "
	}
Else
	{
	$enddate = "Newest record in the database"
	}
</pre>
<p>The code is able to correctly parse the presence of a start date, end date, or both.  Now the following can be added to the end of the code block in the SQL query that makes up the derived table (the part in the nested parentheses):  <span class="flobeecode" id="codekeyword">+</span> <span class="flobeecode" id="codevariable">$start</span> <span class="flobeecode" id="codekeyword">+</span> <span class="flobeecode" id="codevariable">$end</span> <span class="flobeecode" id="codekeyword">+</span>.</p>
<p>Finally, the output is a pretty page with the subtitle that indicates the date range used, the table headers are formatted to visually separate each conversation, and each table is an indicated of a single conversation, regardless of the number of messages shown.</p>
<p>I don&#8217;t expect you to copy and paste all the code above into the script and have it magically work (especially since I made other minor changes to account for the subtitle, additional arguments, etc.), so you can download the entire script below or on the <a href="http://www.flobee.net/downloads">Downloads</a> page.Note: There is a file embedded within this post, please visit this post to download the file.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/ocs-archiving-reporter-group-by-conversation-filter-dates/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

