<?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 &#187; inline code</title>
	<atom:link href="http://www.flobee.net/tag/inline-code/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, 20 Jan 2012 17:04:46 +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>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>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>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>7</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>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. (You can&#8217;t do it as a [...]]]></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.  (You can&#8217;t do it as a one-liner because the <span class="flobeecode" id="codeplain">System.Net.WebClient</span> class doesn&#8217;t have any static methods, which means you have to load the class before you can call any of the methods.)</p>
<pre class="brush:ps;collapse:false;light:true;auto-links:false">
$webclient = new-object System.Net.WebClient
$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>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>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://communicationsserverteam.com/archive/2009/09/28/584.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>
		<item>
		<title>How to clear the mail attribute using PowerShell</title>
		<link>http://www.flobee.net/how-to-clear-the-mail-attribute-using-powershell/</link>
		<comments>http://www.flobee.net/how-to-clear-the-mail-attribute-using-powershell/#comments</comments>
		<pubDate>Mon, 09 Nov 2009 16:09:58 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2003]]></category>
		<category><![CDATA[Exchange 2007]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=409</guid>
		<description><![CDATA[I have been struggling to delete the value in the mail attribute after a mailbox has been deleted. Exchange populates the mail attribute when a mailbox is created (even though Exchange has no use for the attribute), but doesn&#8217;t clear the attribute when the mailbox is deleted. With ADUC integration removed in Exchange 2007, a [...]]]></description>
			<content:encoded><![CDATA[<p>I have been struggling to delete the value in the mail attribute after a mailbox has been deleted.  Exchange populates the mail attribute when a mailbox is created (even though Exchange has no use for the attribute), but doesn&#8217;t clear the attribute when the mailbox is deleted.  With ADUC integration removed in Exchange 2007, a quick way to know if an account has a mailbox is to look at the mail attribute.  But if removing a mailbox no longer clears that attribute, it is difficult know (just by looking at a user account in ADUC) if the account still has a mailbox.</p>
<p>Since Exchange doesn&#8217;t use the mail attribute, you can&#8217;t use the <span class="flobeecode" id="codekeyword">Set-Mailbox</span> attribute, especially if the mailbox is deleted anyway.  I tried using <span class="flobeecode" id="codekeyword">Set-User</span> with the <span class="flobeecode" id="codekeyword">-WindowsEmailAddress</span> parameter, but because the data type is <span class="flobeecode" id="codeplain">Microsoft.Exchange.Data.SmtpAddress</span>, setting the value to &quot;&quot; or <span class="flobeecode" id="codevariable">$null</span> doesn&#8217;t work because those aren&#8217;t properly formatted SMTP addresses.</p>
<p>So, I figured I needed to get away from any Exchange cmdlet.  I used PowerShell&#8217;s native support for ADSI to bind to the user object: <span class="flobeecode" id="codekeyword">New-Object</span> <span class="flobeecode" id="codeplain">DirectoryServices.DirectoryEntry</span> <span class="flobeecode" id="codestring">&quot;LDAP://UserDN&quot;</span>.  But you will get an error if you try to set the attribute to null (<span class="flobeecode" id="codevariable">$user</span><span class="flobeecode" id="codeplain">.mail = </span><span class="flobeecode" id="codevariable">$null</span>).  You can set it to an empty value (&quot;&quot;), but you will then get an error when you try to commit the change: <span class="flobeecode" id="codevariable">$user</span><span class="flobeecode" id="codeplain">.SetInfo()</span>.</p>
<p>How can you possibly clear this attribute, one that is so easy to do in ADUC just by deleting the value in it?  It is necessary to fall back to the <span class="flobeecode" id="codeplain">PutEx</span> method.  Using that will let you use the <span class="flobeecode" id="codeconstant">ADS_PROPERTY_CLEAR</span> constant (indicated by the numeric one in the first argument).  It has taken me days to finally get to this point, so hopefully this post will shorten that time for others trying to do the same thing.</p>
<pre class="brush:ps;gutter:false;collapse:false">
$user = Get-User "username"
$ldapDN = "LDAP://" + $user.distinguishedName
$adUser = New-Object DirectoryServices.DirectoryEntry $ldapDN
$adUser.PutEx(1, "mail", $null)
$adUser.SetInfo()
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/how-to-clear-the-mail-attribute-using-powershell/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Add caching to mailbox creation load balancing script</title>
		<link>http://www.flobee.net/add-caching-to-mailbox-creation-load-balancing-script/</link>
		<comments>http://www.flobee.net/add-caching-to-mailbox-creation-load-balancing-script/#comments</comments>
		<pubDate>Tue, 22 Sep 2009 19:57:52 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2007]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=135</guid>
		<description><![CDATA[In a previous post I instructed how to load balance new mailboxes across databases. In a large environment, however, this determination can take upwards of 90 seconds. Normally I don&#8217;t run the mailbox creation script at my company (our IT Security department does), but I ran it yesterday and it took 55 seconds to determine [...]]]></description>
			<content:encoded><![CDATA[<p>In a <a href="http://www.flobee.net/load-balance-new-mailbox-creation-across-multiple-databases-and-servers-using-powershell/">previous post</a> I instructed how to load balance new mailboxes across databases.  In a large environment, however, this determination can take upwards of 90 seconds.  Normally I don&#8217;t run the mailbox creation script at my company (our IT Security department does), but I ran it yesterday and it took 55 seconds to determine the database.  That&#8217;s just too long for me.</p>
<p>So I decided to add what was going to be my fallback option in the first place: using a cached list.  This code uses an xml file to keep track of the database to use at a given location and a timestamp of when that entry was last updated.  The xml file uses the following format:</p>
<p>
<pre class="brush:xml;gutter:false;wrap-lines:false;auto-links:false;collapse:false">
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;sites&gt;
	&lt;site id=&quot;Name1&quot;&gt;
		&lt;database name=&quot;Server1\Storage Group 1\Mailbox Store 1&quot; /&gt;
		&lt;timestamp time=&quot;2009-09-22T08:59:29.4878574-07:00&quot; /&gt;
	&lt;/site&gt;
	&lt;site id=&quot;Name2&quot;&gt;
		&lt;database name=&quot;Server2\Storage Group 5\Mailbox Store 1&quot; /&gt;
		&lt;timestamp time=&quot;2009-09-22T08:17:51.5632031-07:00&quot; /&gt;
	&lt;/site&gt;
....
&lt;/sites&gt;
</pre>
<p>Set the $sourcexml variable to the full path to the file.  The function that calls this code should set the $Location parameter to whatever value is stored in the id attribute of the site nodes.  The xml file is opened, the timestamp for a given site name is returned, and compared to a time interval (in this case, 24 hours, but you can set it to anything you want).  If less than 24 hours old, the database name in the site node is used.  If more than 24 hours old, it will run the code to determine the database to use (from the other post).  After the determination is made, it writes the database name and the current time back into the xml file and saves it.</p>
<p>The on-demand query for one site ran in 53 seconds through mailbox creation for the first run, but created the second mailbox in 11 seconds.  That is much more tenable, at least for an impatient person like myself.</p>
<pre class="brush:ps;gutter:false;wrap-lines:false;auto-links:false;collapse:false">
$start = Get-Date
$sourcexml = "Path to input xml file.xml"
[xml]$db = Get-Content $sourcexml
$site = $db.sites.site | where {$_.id -eq $Location}
[datetime]$lastUpdate = $site.Timestamp.Time
if (($start - $lastUpdate).TotalHours -lt 24)
	{
	Write-Host "Timstamp for selected site is less than 24 hours old.  Using cached entry."
	$site.database.name
	}
else
	{
	Write-Host "Timestamp for selected site is more than 24 hours old.  Calculating database to use."
	#Run code here to determine database to use
	#
	#When done, continue
	$site.database.name = ($mailboxcount.GetEnumerator() | sort value | select -first 1).key
	$site.timestamp.time = (Get-Date -Format o).ToString()
	$db.save($sourcexml)
	$site.database.name
	}
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/add-caching-to-mailbox-creation-load-balancing-script/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Get automatic notification when running low on BES licenses</title>
		<link>http://www.flobee.net/get-automatic-notification-when-running-low-on-bes-licenses/</link>
		<comments>http://www.flobee.net/get-automatic-notification-when-running-low-on-bes-licenses/#comments</comments>
		<pubDate>Thu, 30 Jul 2009 23:57:48 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[VBScript]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=81</guid>
		<description><![CDATA[This VBScript uses native SQL connectivity via ADO to query your BlackBerry Enterprise Server configuration database for the current number of users. Since the license total is not stored in the database you have to set it as a variable in the script. It then compare the two numbers and if your defined threshold is [...]]]></description>
			<content:encoded><![CDATA[<p>This VBScript uses native SQL connectivity via ADO to query your BlackBerry Enterprise Server configuration database for the current number of users.  Since the license total is not stored in the database you have to set it as a variable in the script.  It then compare the two numbers and if your defined threshold is exceeded, it will email you a notification.  Just schedule the script to run daily.  Download a zipped version <a href="/download/BESLicenseMonitor.zip">here</a>, or copy below.</p>
<pre class="brush:vb;gutter:false;wrap-lines:false;auto-links:false;collapse:true">
'BES license usage monitor.  Set current license total, alert threshold, And
'other variables to be notified when the license usage exceeds the threshold.
Dim strSQLServer, strBESDB, iCurrentLicenseTotal, iLicenseThreshold
Dim strMailRecipients, strMailSender, strMailSubject, strMailServer
'*************************************
'------Configurable Variables---------
iCurrentLicenseTotal = 1000 'Total number of licenses in License Management
iLicenseThreshold = 20 'Free licenses threshold for notification
strSQLServer = "ServerName" 'NetBIOS name of SQL Server
strBESDB = "DatabaseName" 'Database name in SQL
'------Notification configuration-----
strMailRecipients = "user1@domain.com,user2@domain.com" 'Command-separated list of addresses
strMailSender = "BESLicenseCounter@domain.com" 'From address
strMailSubject = "BES license usage at critical level" 'Subject of notification message
strMailServer = "smtpserver.domain.com" 'FQDN of smarthost
'*************************************

Dim iCurrentUserTotal, iAvailableLicenses, strSQLQuery, strSQLConn
Dim objSQLConn, objSQLRecordSet, objSQLExec
strSQLConn = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Data Source=" &#038; _
	strSQLServer &#038; "; Initial Catalog=" &#038; strBESDB
strSQLQuery = "Select Count(*) As Total_Users From UserStats"
Set objSQLConn = CreateObject("ADODB.Connection")
Set objSQLRecordSet = CreateObject("ADODB.Recordset")
objSQLConn.Open = strSQLConn
objSQLRecordSet.Open strSQLQuery, objSQLConn
Set objSQLExec = objSQLRecordSet
While Not objSQLExec.EOF
	iCurrentUserTotal = objSQLExec.Fields("Total_Users").Value
	objSQLExec.MoveNext
Wend
iAvailableLicenses = iCurrentLicenseTotal - iCurrentUserTotal
If iAvailableLicenses &lt; iLicenseThreshold Then
	Dim strMailBody
	strMailBody = iCurrentUserTotal &#038; " of " &#038; iCurrentLicenseTotal &#038; " licenses are in use."
	strMailBody = strMailBody &#038; "  It is time to order more licenses."
	SendMail
End If

Sub SendMail
	Set objEmail = CreateObject("CDO.Message")
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = strMailServer
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
	objEmail.Configuration.Fields.Update
	objEmail.From = strMailSender
	objEmail.To = strMailRecipients
	objEmail.Subject = strMailSubject
	objEmail.HTMLBody = strMailBody
	objEmail.Send
End Sub
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/get-automatic-notification-when-running-low-on-bes-licenses/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Load balance new mailbox creation across multiple databases and servers using PowerShell</title>
		<link>http://www.flobee.net/load-balance-new-mailbox-creation-across-multiple-databases-and-servers-using-powershell/</link>
		<comments>http://www.flobee.net/load-balance-new-mailbox-creation-across-multiple-databases-and-servers-using-powershell/#comments</comments>
		<pubDate>Wed, 27 May 2009 21:42:29 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2007]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=64</guid>
		<description><![CDATA[I needed a way to have new mailboxes be automatically distributed across a list of databases on multiple servers, and I recalled seeing a script somewhere.  The blog post with those details can be found here.  As the author points out, it can take a long time to build the hash table, too long for [...]]]></description>
			<content:encoded><![CDATA[<p>I needed a way to have new mailboxes be automatically distributed across a list of databases on multiple servers, and I recalled seeing a script somewhere.  The blog post with those details can be found <a title="Load balance mailbox creation" href="http://jonoble.spaces.live.com/Blog/cns!CC73D8744F0894A5!629.entry" target="_blank">here</a>.  As the author points out, it can take a long time to build the hash table, too long for my requirements.  For example, one of my longer queries builds the table for 9 databases with over 5000 mailboxes in 58 seconds.  That is just way too  long for someone to feed a user account into to get a mailbox created.</p>
<p>So I broke down the query being used to find where efficiency could be improved.  The Get-ExchangeServer cmdlet slows the script down and can be avoided altogether by feeding the server list directly into the Get-MailboxDatabase cmdlet.  Additionally, I discovered just how inefficient the Exchange cmdlets are.  I already knew they were based on running other cmdlets for targets servers across a WAN that return not a lot of data, but this reinforced those findings.</p>
<p>To get away from the Exchange cmdlets, I query AD directly by instantiating ADSI in .NET manually.  Instead of slowly building the hash table I just take the record count of the search results and populate the table entry at once.  This reduced the time to completion from 58 seconds to 24 seconds.  These findings are consistent across different database counts and mailbox totals: 60% reduction in time.  While still too long for my taste, it certainly is much better than before and is within reason for the people at my company that make use of the script.</p>
<p>Since my script encompasses more than just the load balancing part (I create a GUI form where the location for the mailbox is selected, which feeds a list of appropriate servers to the function), this is just the part that creates the hash table with the database counts and returns the database with the lowest count:</p>
<pre class="brush:ps;gutter:false;wrap-lines:false;auto-links:false;collapse:false">
$mbxServers = @()
$mailboxcount = @{}
$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$root = [ADSI]"GC://$($domain.Name)"
$mbxServers | %{Get-MailboxDatabase -server $_} | ?{$_.recovery -eq $FALSE} | %{
$filter = "(&amp;(objectcategory=user)(homeMDB=" + ($_.DistinguishedName) + "))"
$search = new-Object System.DirectoryServices.DirectorySearcher($root,$filter)
$search.PropertiesToLoad.Add("homeMDB") | Out-Null
$result = $search.FindAll()
$mailboxcount["$($_.Identity)"] = $result.count
}
($mailboxcount.GetEnumerator() | sort value | select -first 1).key
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/load-balance-new-mailbox-creation-across-multiple-databases-and-servers-using-powershell/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>PowerShell script to report last successful full backup of Exchange 2007</title>
		<link>http://www.flobee.net/powershell-script-to-report-last-successful-full-backup-of-exchange-2007/</link>
		<comments>http://www.flobee.net/powershell-script-to-report-last-successful-full-backup-of-exchange-2007/#comments</comments>
		<pubDate>Tue, 22 Jul 2008 14:28:27 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2003]]></category>
		<category><![CDATA[Exchange 2007]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=297</guid>
		<description><![CDATA[Edit: The inline code in this post is not the latest version of the script. Get the latest version from the downloads page. This script is a port of my original backup report that was written in VBScript.&#160; That script reports on both 2003 and 2007 servers, but lacked some of the features that I [...]]]></description>
			<content:encoded><![CDATA[<p><font color="red">Edit:  The inline code in this post is not the latest version of the script.  Get the latest version from the <a href="/downloads">downloads</a> page.</font></p>
<p>This script is a port of my <a href="http://www.flobee.net/updated-last-backup-report-script/">original backup report</a> that was written in VBScript.&nbsp; That script reports on both 2003 and 2007 servers, but lacked some of the features that I wanted to put in.&nbsp; PowerShell natively supports date-awareness, which makes it much easier to add the number one feature I wanted to add: highlighting servers that haven&#8217;t had backups since a specified period of time.</p>
<p>Because I am using the native Exchange cmdlets instead of WMI or CDOEXM, this only reports on Exchange 2007 servers.&nbsp; I figure accommodating both is more work than it is worth, so I just modified my VBScript version to not include any server in the Exchange 12 admin group and I have both run every day until my migration to 2007 is complete.</p>
<p>The script reports the last successful full backup of any Exchange 2007 server with the mailbox role installed.&nbsp; It checks for the presence of storage groups and databases within them.&nbsp; It notes if a backup is currently in progress, as well as if a backup has never completed.&nbsp; If a backup has not completed in the last 72 hours (modifiable), it is highlighted in red so it is easy to spot.&nbsp; If a backup is less than the defined number of hours old, I use the Marlett font to display a green checkmark.&nbsp; This allows for a checkmark without having to reference an external image or embed one.&nbsp; Lastly, the report is emailed.&nbsp; The script is shown below, but you can also just <a href="http://www.flobee.net/download/LastBackupReport.zip">download</a> it.</p>
<pre class="brush:ps;gutter:false;wrap-lines:false;auto-links:false;collapse:true">
#Last Backup Report for Exchange 2007 servers
#Version 1.0 - 7/9/08
#--------------------------------------------

#Begin customization-------------------------
$SmtpServer = "server.domain.com" #Enter FQDN of SMTP server
$SmtpFrom = "Exchange Backups &lt;exchangebackupreport@domain.com&gt;" #Enter sender email address
$SmtpTo = "user1@domain.com","user2@domain.com" #Enter one or more recipient addresses in an array
$SmtpSubject = "Exchange 2007 Last Backup Report" #Enter subject of message
$iNomHours = "72" #Enter number of hours since last backup that requires attention
#End customization---------------------------

$date = Get-Date
$sSpace = "&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;"
$sOutput = "&lt;table&gt;"

#Checkmark to indicate last backup within nominal time
$sCheckMark = "&lt;span style=""font-family: Marlett; color: green; font-size: 14pt; font-weight: bold""&gt;a&lt;/span&gt;"

#Retrieve Exchange servers with mailbox role
$ExServer = Get-ExchangeServer | where {$_.IsMailboxServer -eq $True} | Sort-Object Name
Foreach ($server in $ExServer)
	{
	$sOutput += "&lt;tr&gt;&lt;td&gt;&lt;font size=2&gt;&lt;u&gt;&lt;b&gt;$server&lt;/b&gt;&lt;/u&gt;&lt;/font&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;"
	#Retrieve storage groups for a given server
	$StorageGroup = $server | Get-StorageGroup | Sort-Object Name
	#Check for absence of any storage groups
	If (($StorageGroup | Measure-Object Name).Count -eq $null)
		{
		$sOutput += "&lt;tr&gt;&lt;td&gt;&lt;font size=2&gt;" + $sSpace + "No storage groups present.&lt;/font&gt;&lt;/td&gt;&lt;/tr&gt;"
		}
	Else
		{
		Foreach ($sg in $StorageGroup)
			{
			$sOutput += "&lt;tr&gt;&lt;td&gt;&lt;font size=2&gt;" + $sSpace + $sg.Name + "&lt;/font&gt;&lt;/td&gt;&lt;/tr&gt;"
			#Retrieve mailbox databases for a given storage group
			$MailboxDatabase = $StorageGroup | Get-MailboxDatabase -Status | Sort-Object Name
			#Check for absence of any databases in storage group
			If (($MailboxDatabase | Measure-Object Name).Count -eq $null)
				{
				$sOutput += "&lt;tr&gt;&lt;td&gt;&lt;font size=2&gt;" + $sSpace + $sSpace + "No mailbox stores present.&lt;/font&gt;&lt;/td&gt;&lt;/tr&gt;"				}
			Else
				{
				Foreach ($db in $MailboxDatabase)
					{
					$sBackupRunning = ""
					#Note if backup is currently running
					If ($db.BackupInProgress -eq $true)
						{$sBackupRunning = "&lt;font size=2 color=blue&gt;(Backup In Progress)&lt;/font&gt;"}
					#Determine if backup has ever completed
					If ($db.LastFullBackup -ne $null)
						{
						$sBackupDay = $db.LastFullBackup.get_DayofWeek()
						$sBackupDateTime = $db.LastFullBackup.ToString("g")
						#Flag if last completed backup started longer than defined variable
						If (($date - $db.LastFullBackup).TotalHours -gt $iNomHours)
							{
							$sLastBackup = "&lt;font size=2&gt;Last Backup Started At: &lt;font color=red&gt;" + $sBackupDay + ", " + $sBackupDateTime + "&lt;/font&gt;&lt;/font&gt;"
							}
						Else
							{
							$sLastBackup = "&lt;font size=2&gt;Last Backup Started At: " + $sBackupDay + ", " + $sBackupDateTime + " &lt;/font&gt;" + $sCheckmark
							}
						}
					Else
						{
						$sLastBackup = "&lt;font size=2&gt;No full backup has completed yet.&lt;/font&gt;"
						}
					$sOutput += "&lt;tr&gt;&lt;td&gt;&lt;font size=2&gt;" + $sSpace + $sSpace + $db.Name + " &lt;/font&gt;&lt;/td&gt;&lt;td&gt;" + $sLastBackup + $sBackupRunning + "&lt;/td&gt;&lt;/tr&gt;"
					}
				}
			}
		}
	}
$sOutput += "&lt;/table&gt;"

#Email results
$SmtpClient = New-Object System.Net.Mail.SmtpClient
$MailMessage = New-Object System.Net.Mail.MailMessage
$SmtpClient.Host = $SmtpServer
$MailMessage.From = $SmtpFrom
Foreach ($address in $smtpTo)
	{$MailMessage.To.Add($address)}
$MailMessage.Subject = $SmtpSubject
$MailMessage.IsBodyHTML = $true
$MailMessage.Body = $sOutput
$SmtpClient.Send($MailMessage)
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/powershell-script-to-report-last-successful-full-backup-of-exchange-2007/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Script to format an LDAP filter for readability</title>
		<link>http://www.flobee.net/script-to-format-an-ldap-filter-for-readability/</link>
		<comments>http://www.flobee.net/script-to-format-an-ldap-filter-for-readability/#comments</comments>
		<pubDate>Tue, 07 Aug 2007 14:32:07 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[VBScript]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=226</guid>
		<description><![CDATA[I have automated DLs that use LDAP filters for&#160;membership criteria.&#160; These filters are stored as binary data in the extensionData attribute of the group object, so they are not easily accessible by users who want to know what filter is being applied to a DL they own. It was easy enough to extract the LDAP [...]]]></description>
			<content:encoded><![CDATA[<p>I have automated DLs that use LDAP filters for&nbsp;membership criteria.&nbsp; These filters are stored as binary data in the extensionData attribute of the group object, so they are not easily accessible by users who want to know what filter is being applied to a DL they own.</p>
<p>It was easy enough to extract the LDAP filter from the attribute, convert it to a string value, and display it.&nbsp; But users don&#8217;t generally know how to read LDAP filters, let alone represented as a long line of text.&nbsp; So I started looking for utilities or scripts that would take an LDAP filter, parse it, and display it with nesting.&nbsp; Uh, yeah, there aren&#8217;t any.&nbsp; I was determined, and so countless hours later I have a function that do such a thing.</p>
<p>The difficult part was keeping track of the level of nesting/indentation at any point. Since a filter can be written in almost any order as long as the resulting equation equals what you want, I couldn&#8217;t use any kind of static detection. Just because an open parentheses is following by another one doesn&#8217;t mean that you are, say, three levels nested. So the important part of the script keeps track of the indentation level as the cursor position moves through the filter.</p>
<p>I replace ampersands in the filter with crosshatches while working with it because I was having weird results otherwise, most likely because the ampersand is an operator in both LDAP and VBScript.  And because the output is to an IE window and the indentation uses non-breaking spaces instead of the traditional tab (which doesn&#8217;t exist in HTML), I substitute the HTML string for a non-breaking space with a unique string of letters so the ampersand in that doesn&#8217;t get in the way.</p>
<p>Because this is part of a bigger script, I pulled out just the portion that formats the LDAP filter and displays it in an IE window.  You just need to provide the &quot;raw&quot; filter, whether directly in the script or some other method. Copy and paste the code below or download it <a href="http://www.flobee.net/download/ParseLDAPFilter.zip" title="Parse LDAP filter for readbility">here</a>.</p>
<pre class="brush:vb;gutter:false;wrap-lines:false;auto-links:false;collapse:true">
Option Explicit
Dim strLDAPFilter, strFormattedFilter
Dim objIE, objDoc
strLDAPFilter = &quot;YourLDAPFilter&quot;
Set objIE =  CreateObject(&quot;InternetExplorer.Application&quot;) &nbsp;&nbsp;objIE.AddressBar = False
objIE.Menubar = False
objIE.Toolbar = False
objIE.Resizable = True
objIE.Height = 450
objIE.Width = 700
objIE.Visible = True
objIE.Navigate(&quot;about:blank&quot;)
While objIE.Busy
	WScript.Sleep 100
Wend

Set objDoc = objIE.Document
objDoc.Open
objDoc.Write(&quot;&lt;TITLE&gt;LDAP Filter Display&lt;/TITLE&gt;&quot;)
objDoc.Write(&quot;&lt;BODY BGCOLOR=#C0C0C0&gt;&quot;)

'Parse the LDAP filter and format results for display
'Apply nesting for readability
strFormattedFilter = FormatLDAPDisplay(strLDAPFilter)
objDoc.Write(strFormattedFilter)
'Format single-line LDAP filter to include nesting
Function FormatLDAPDisplay(strLDAPFilter)
	'Replace ampersands with crosshatches to keep them from interfering
	strLDAPFilter = Replace(strLDAPFilter, Chr(38), Chr(35))

	'Iterate through each character in filter and insert CRLF and nesting
	Dim intPos, intIndentCount, intWhileIndent
	Dim strIndentation, strInsert, strCharacter, strNewLDAPFilter
	Dim bolDblClose
	intPos = 1
	intIndentCount = 0
	Do While intPos &lt; Len(strLDAPFilter)
		strCharacter = Mid(strLDAPFilter, intPos, 1)
		intWhileIndent = 1
		strIndentation = &quot;&quot;
		Select Case strCharacter
			'LDAP operators to watch for to modify nesting level.
			'NOT operator ignored because only used in one-off attribute value
			Case Chr(35), Chr(124)
				'Operator followed by open paren means nesting increase
				If Mid(strLDAPFilter, intPos + 1, 1) = Chr(40) Then
					intIndentCount = intIndentCount + 1
					'Build nest based on number of indentations
					Do While intWhileIndent &lt;= intIndentCount
						'Use unique string as placeholder for nesting with HTML spaces
						strIndentation = strIndentation &amp; &quot;QZNBSPQZNBSPQZNBSPQZNBSP&quot;
						intWhileIndent = intWhileIndent + 1
					Loop
					'Insert new string for formatting
					strNewLDAPFilter = Replace(strLDAPFilter, strCharacter, strCharacter &amp; &quot;&lt;br&gt;&quot; &amp; strIndentation, intPos, 1)
					'Restore full filter including new string
					strLDAPFilter = Left(strLDAPFilter, intPos - 1) &amp; strNewLDAPFilter
					'Move current position to next character after inserted string
					intPos = intPos + Len(strIndentation) + 6
				Else
					intPos = intPos + 1
				End If
			Case Chr(40)
				Do While intWhileIndent &lt;= intIndentCount
					strIndentation = &quot;QZNBSPQZNBSPQZNBSPQZNBSP&quot; &amp; strIndentation
					intWhileIndent = intWhileIndent + 1
				Loop
				If Not strIndentation = &quot;&quot; Then
					'If open paren follows close paren, insert CRLF
					If Mid(strLDAPFilter, intPos - 1, 1) = Chr(41) Then
						strIndentation = strIndentation &amp; &quot;&lt;br&gt;&quot;
					End If
					strNewLDAPFilter = Replace(strLDAPFilter, strCharacter, strIndentation &amp; strCharacter, intPos, 1)
					strLDAPFilter = Left(strLDAPFilter, intPos - 1) &amp; strNewLDAPFilter
					intPos = intPos + Len(strIndentation) +1
				Else
					intPos = intPos + 1
				End If
			Case Chr(41)
				'Two consecutive close paren means nesting reduces one level
				If Mid(strLDAPFilter, intPos + 1, 1) = Chr(41) Then
					intIndentCount = intIndentCount - 1
					bolDblClose = True
				End If
				Do While intWhileIndent &lt;= intIndentCount
					strIndentation = strIndentation &amp; &quot;QZNBSPQZNBSPQZNBSPQZNBSP&quot;
					intWhileIndent = intWhileIndent + 1
				Loop
				strNewLDAPFilter = Replace(strLDAPFilter, strCharacter, strCharacter &amp; &quot;&lt;br&gt;&quot; &amp; strIndentation, intPos, 1)
				strLDAPFilter = Left(strLDAPFilter, intPos - 1) &amp; strNewLDAPFilter
				'Adjust position to account for two close paren
				If bolDblClose = True Then
					intPos = intPos + Len(strIndentation) + 5
					bolDblClose = Empty
				Else
					intPos = intPos + Len(strIndentation) + 6
				End If
			Case Else
				'No paren or operator means move to next character
				intPos = intPos + 1
		End Select
	Loop

	'Replace LDAP operators with words
	strLDAPFilter = Replace(strLDAPFilter, Chr(35), &quot;&lt;i&gt;AND&lt;/i&gt;&quot;)
	strLDAPFilter = Replace(strLDAPFilter, Chr(124), &quot;&lt;i&gt;OR&lt;/i&gt;&quot;)
	strLDAPFilter = Replace(strLDAPFilter, Chr(33), &quot;&lt;i&gt;NOT &lt;/i&gt;&quot;)
	'Replace spaceholders with HTML spaces
	strLDAPFilter = Replace(strLDAPFilter, &quot;QZNBSP&quot;, Chr(38) &amp; &quot;nbsp;&quot;)
	FormatLDAPDisplay = strLDAPFilter
End Function

Set objIE = Nothing
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/script-to-format-an-ldap-filter-for-readability/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Add comment notification to simplebog 3.0</title>
		<link>http://www.flobee.net/add-comment-notification-to-simplebog-3-0/</link>
		<comments>http://www.flobee.net/add-comment-notification-to-simplebog-3-0/#comments</comments>
		<pubDate>Mon, 30 Jul 2007 18:52:43 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[VBScript]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=220</guid>
		<description><![CDATA[Since I am using approval for comments on my site, I had no way of knowing when someone posted a comment pending approval.&#160; And since you can&#8217;t simply look for comments to posts without going into the database directly, I needed a way to know when someone has posted a comment and to which post [...]]]></description>
			<content:encoded><![CDATA[<p>Since I am using approval for comments on my site, I had no way of knowing when someone posted a comment pending approval.&nbsp; And since you can&#8217;t simply look for comments to posts without going into the database directly, I needed a way to know when someone has posted a comment and to which post it belongs.</p>
<p>To do this, add this subroutine to the end of functions.asp, which is the CDO code to send a message.&nbsp; I didn&#8217;t use variables that are assigned in config.asp, so you will have to set them in the subroutine directly for SMTP server, from address, to address, and domain name in the body.&nbsp;</p>
<pre class="brush:vb;gutter:false;wrap-lines:false;auto-links:false;collapse:false">
&lt;%
'Send comment notification
Sub SendEmail (strBDate, strBID)
	Dim objMail
	Set objMail = CreateObject(&quot;CDO.Message&quot;)
	objMail.Configuration.Fields.Item (&quot;http://schemas.microsoft.com/cdo/configuration/sendusing&quot;)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 2
	objMail.Configuration.Fields.Item (&quot;http://schemas.microsoft.com/cdo/configuration/smtpserver&quot;)&nbsp;&nbsp;&nbsp;&nbsp; = &quot;SMTP hostname&quot;
	objMail.Configuration.Fields.Item (&quot;http://schemas.microsoft.com/cdo/configuration/smtpserverport&quot;) = 25
	objMail.Configuration.Fields.Update
	objMail.From&nbsp;&nbsp;&nbsp;&nbsp; = &quot;fromaddress@yourdomain.com&quot;
	objMail.To&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = &quot;toaddress@yourdomain.com&quot;
	objMail.Subject&nbsp; = &quot;Comment has been submitted for approval&quot;&nbsp;&nbsp;&nbsp;
	objMail.HTMLBody = &quot;A comment has been submitted for approval.&nbsp; Go to http://www.yourdomain.com/admin?cmd=bloglist&amp;view=calendar&amp;blogDate=&quot; &amp; strBDate &amp; &quot;&amp;comments=&quot; &amp; strBID &amp; &quot; to approve.&quot;
	objMail.Send
	Set objMail = Nothing
End Sub
%&gt;
</pre>
<p>Then you need to add code to functions.asp to call this subroutine from the subroutine that inserts the comment.&nbsp; Find the subroutine labeled InsertComment(), which should be around line 475 depending on other mods of mine or your own that you may have inserted.&nbsp; Right before the existing line:&nbsp;</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:475">
Response.Redirect(&quot;default.asp?view=plink&amp;id=&quot; &amp; bID &amp; &quot;&amp;comments=1&quot;)
</pre>
<p>insert this code:</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:474">
' convert blog ID to blog Date
strSQL = &quot;SELECT * FROM&nbsp; T_WEBLOG WHERE id = &quot; &amp; bID &amp; &quot; ORDER BY id DESC&quot;
Set Rs = Server.CreateObject(&quot;ADODB.Recordset&quot;)
Rs.ActiveConnection = strConn
Rs.Source = strSQL
Rs.CursorType = 0
Rs.CursorLocation = 2
Rs.LockType = 1
Rs.Open()

If Not rs.EOF Then
	rs.MoveFirst
	While Not rs.EOF
		strBDate = rs(&quot;b_date&quot;)
		rs.MoveNext
	Wend
End If

SendEmail strBDate, bID
</pre>
<p>This is necessary because the comments are not accessed by using the blog entry ID, but the blog entry&#8217;s date.&nbsp; So it is necessary to cross-reference the entry ID to the entry date, and then link to the comments for a given entry ID on that date.&nbsp; The last line calls the email function which will include a hyperlink to the comments for the blog entry that has a new comment.</p>
<p>Now, that is all fine and dandy.&nbsp; You will receive the email with the link, but when you follow it to your site, you probably won&#8217;t have an active session so you will have to log in.&nbsp; The way the admin default and login pages work, you will lose the link to the entry you need to approve so you have to click the link in the email again.&nbsp; I work around this by implementing the use of query strings so the site remembers where you were trying to go before you had to log in.</p>
<p>To add this feature, edit admindefault.asp.&nbsp; At line 8, replace the Response.Redirect line with the following code:&nbsp;</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:8">
Dim sProtocol, sDomain, sPath, sQuerystring, sResult
sProtocol = &quot;http://&quot;
If UCase(Request.ServerVariables(&quot;HTTPS&quot;)) = &quot;ON&quot; Then
	sProtocol = &quot;https://&quot;
	sDomain = LCase(Request.ServerVariables(&quot;SERVER_NAME&quot;))
	sPath = LCase(Request.ServerVariables(&quot;SCRIPT_NAME&quot;))
	sQuerystring = LCase(Request.Querystring)
	sResult = sProtocol &amp; sDomain &amp; sPath
	If Len(sQuerystring) &gt; 0 Then
		sResult = sResult &amp; &quot;?&quot; &amp; sQuerystring
		sResult = Server.URLEncode(sResult)
		Response.Redirect(&quot;login.asp&quot;) &amp; &quot;?sURL=&quot; &amp; sResult
</pre>
<p>This builds a query string of the URL you were going to go to before you are redirected to the login page.&nbsp; Now edit adminlogin.asp.&nbsp; At line 34, before the If statement to check for a postback, insert the following line:&nbsp;</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:34">
strSourceURL = Request.QueryString(&quot;sURL&quot;)
</pre>
<p>This retrieves the query string and puts it into a variable.&nbsp; At line 39, before the SQL statement to select the users from the database, insert the following code:&nbsp;</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:39">
If Not Trim(Request.Form(&quot;sourceURL&quot;)) = &quot;&quot; Then
	strSourceURL = Request.Form(&quot;sourceURL&quot;)
Else
	strSourceURL = &quot;./?&quot;
End If
</pre>
<p>This extra code is added to accommodate you entering a bad password or if you are logging in without following a comment link.&nbsp; At line 74, comment out Johann&#8217;s Response.Redirect line after a successful login and then insert and replace the Response.Redirect for no user found with this:&nbsp;</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:74">
	Response.Redirect(strSourceURL)
Else
	Response.Redirect(&quot;login.asp?error=nouser&quot;) &amp; &quot;&amp;sURL=&quot; &amp; Server.URLEncode(strSourceURL)
</pre>
<p>This sends you to the page you were intending to go to in the first place.&nbsp; If you entered a bad password, this also preserves the original URL after the login page reloads.</p>
<p>Last, but not least, you need to a hidden form field that will store the original URL when you submit the form to log in.&nbsp; Near the end of the file, insert a line before the close form tag and paste this:&nbsp;</p>
<pre class="brush:vb;gutter:false;wrap-lines:false;auto-links:false;collapse:false">
&lt;input name=&quot;sourceURL&quot; type=&quot;hidden&quot; value=&quot;&lt;%= strSourceURL %&gt;&quot; /&gt;
</pre>
<p>I hope that&#8217;s not too complicated to follow.&nbsp; It&#8217;s a little more convoluted for this modification because it is necessary to add code in multiple places in multiple files, rather than just a chunk of code in one file.&nbsp; But, now you will receive an email when a comment is posted and be easily able to approve or deny the comment just by following the link in the email.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/add-comment-notification-to-simplebog-3-0/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Updated: Copy DLs from one user to another</title>
		<link>http://www.flobee.net/updated-copy-dls-from-one-user-to-another/</link>
		<comments>http://www.flobee.net/updated-copy-dls-from-one-user-to-another/#comments</comments>
		<pubDate>Sun, 22 Jul 2007 23:01:09 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2003]]></category>
		<category><![CDATA[Exchange 2007]]></category>
		<category><![CDATA[VBScript]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=202</guid>
		<description><![CDATA[The first version of the script really was quick and dirty, requiring you to manually put the source and target users&#8217; DNs in the script.&#160; Since a coworker has been using the script, I thought it appropriate to update it to prompt for the usernames.&#160; In addition, I added a new feature I recently read [...]]]></description>
			<content:encoded><![CDATA[<p>The first version of the script really was quick and dirty, requiring you to manually put the source and target users&#8217; DNs in the script.&nbsp; Since a coworker has been using the script, I thought it appropriate to update it to prompt for the usernames.&nbsp; In addition, I added a new feature I recently read about, which is to output the results in real-time to a GUI.&nbsp; This is done by creating an object for IE and writing the output similar to wscript.echo, but with the Write method of the object.</p>
<p>Like the original script, since we use automated DLs, too, I look for an indication that a given DL is a SmartDL and skip it.&nbsp; And I now use PrimalScript to work with my scripts, so I use its packager to make an exectuable.&nbsp; This makes it easier and nicer for non-IT end-users who will be running scripts like these.</p>
<p>Download it <a href="http://www.flobee.net/download/CopyDLMembership.zip">here</a>, or copy/paste below.</p>
<pre class="brush:vb;gutter:false;wrap-lines:false;auto-links:false;collapse:true">
'Version 2.0 - July 23, 2007
'Copy distribution group membership from one user to another,
'excluding automated DLs (SmartDL).
'Get source user
While Not bolExit = True
	strOldSamUser = InputBox(&quot;Enter the sAMAccountName of the person to copy DLs FROM.&quot; _
		, &quot;Enter username&quot;)
	If strOldSamUser = &quot;&quot; Then
		WScript.Quit
	End If

	'Find the Global Catalog server
	Set objCont = GetObject(&quot;GC:&quot;)
	For Each objGC In objCont
		strADsPath = objGC.ADsPath
	Next

	Set objConnection = CreateObject(&quot;ADODB.Connection&quot;)
	Set objRecordset = CreateObject(&quot;ADODB.Recordset&quot;)
	objConnection.Provider = &quot;ADsDSOObject&quot;

	objConnection.Open &quot;ADs Provider&quot;
	strQuery = &quot;&lt;&quot; &amp; strADsPath &amp; &quot;&gt;;(&amp;(objectcategory=user)(sAMAccountName=&quot; &amp; strOldSamUser &amp; _
		&quot;));displayName,distinguishedName;subtree&quot;
	Set objRecordset = objConnection.Execute(strQuery)

	If Trim(objRecordset.Fields(&quot;distinguishedName&quot;)) = &quot;&quot; Then
		strNoUser = MsgBox(&quot;Warning: User cannot be found.&nbsp; Verify sAMAccountName.&quot;, vbCritical, &quot;User not found!&quot;)
		bolExit = False
	Else
		intCorrectUser = MsgBox(&quot;Is this the correct user?&quot; &amp; VbCrLf &amp; VbCrLf &amp; &quot;Display Name: &quot; &amp; _
		objRecordset.Fields(&quot;displayName&quot;) &amp; VbCrLf &amp; &quot;DN: &quot; &amp; objRecordset.Fields(&quot;distinguishedName&quot;), _
			vbYesNo, &quot;Old user?&quot;)
		If intCorrectUser &lt;&gt; 6 Then
			bolExit = False
		Else
			strSrcDN = objRecordset.Fields(&quot;distinguishedName&quot;)
			bolExit = True
		End If
	End If
 Wend
'Open IE to display progress and results
Set objIE = CreateObject(&quot;InternetExplorer.Application&quot;)
objIE.AddressBar = False
objIE.Menubar = False
objIE.Toolbar = False
objIE.Resizable = True
objIE.Left = 10
objIE.Height = 450
objIE.Width = 800
objIE.Visible = True
objIE.Navigate(&quot;about:blank&quot;)
While objIE.Busy
	WScript.Sleep 100
Wend
Set objDoc = objIE.Document
objDoc.Open
objDoc.Write(&quot;&lt;TITLE&gt;Copy DL Membership&lt;/TITLE&gt;&quot;)
objDoc.Write(&quot;&lt;BODY BGCOLOR=#C0C0C0&gt;&quot;)
objDoc.Write(&quot;&lt;P&gt;&lt;b&gt;Source:&lt;/b&gt; &quot; &amp; objRecordset.Fields(&quot;distinguishedName&quot;) &amp; &quot;&lt;br&gt;&quot;)
'Get target user
bolExit = False
While Not bolExit = True
	strNewSamUser = InputBox(&quot;Enter the sAMAccountName of the person to copy DLs TO.&quot; _
		, &quot;Enter username&quot;)
	If strNewSamUser = &quot;&quot; Then
		objIE.Quit
		WScript.Quit
	End If
	strQuery = &quot;&lt;&quot; &amp; strADsPath &amp; &quot;&gt;;(&amp;(objectcategory=user)(sAMAccountName=&quot; &amp; strNewSamUser &amp; _
		&quot;));displayName,distinguishedName;subtree&quot;
	Set objRecordset = objConnection.Execute(strQuery)
	If Trim(objRecordset.Fields(&quot;distinguishedName&quot;)) = &quot;&quot; Then
		strNoUser = MsgBox(&quot;Warning: User cannot be found.&nbsp; Verify sAMAccountName.&quot;, vbCritical, &quot;User not found!&quot;)
		bolExit = False
	Else
		intCorrectUser = MsgBox(&quot;Is this the correct user?&quot; &amp; VbCrLf &amp; VbCrLf &amp; &quot;Display Name: &quot; &amp; _
			objRecordset.Fields(&quot;displayName&quot;) &amp; VbCrLf &amp; &quot;DN: &quot; &amp; objRecordset.Fields(&quot;distinguishedName&quot;), _
			vbYesNo, &quot;Old user?&quot;)
		If intCorrectUser &lt;&gt; 6 Then
			bolExit = False
		Else
			Set objTargetUser = GetObject(&quot;LDAP://&quot; &amp; objRecordset.Fields(&quot;distinguishedName&quot;))
			bolExit = True
		End If
	End If
 Wend
 'Write target user to IE window
 objDoc.Write(&quot;&lt;b&gt;Target:&lt;/b&gt; &quot; &amp; objRecordset.Fields(&quot;distinguishedName&quot;) &amp; &quot;&lt;/P&gt;&quot;)
 'Copy DLs
 strDomFQDN = Mid(strSrcDN, InStr(LCase(strSrcDN), &quot;,dc=&quot;) + 4)
 strGCFQDN = Replace(LCase(strDomFQDN), &quot;,dc=&quot;, &quot;.&quot;)
 Set objOldUser = GetObject(&quot;GC://&quot; &amp; strGCFQDN &amp; &quot;/&quot; &amp; strSrcDN)
 For Each strGroup in objOldUser.MemberOf
	On Error Resume Next
	Set objGroup = GetObject(&quot;LDAP://&quot; &amp; strGroup)
	If Not Trim(objGroup.mailNickname) = &quot;&quot; Then
		If Not Instr(objGroup.info, &quot;SmartDL&quot;) &gt; 0 Then
			objGroup.Add(objTargetUser.ADsPath)
			If Err.Number = 0 Then
				objDoc.Write(objGroup.DisplayName &amp; &quot;: Update successful.&lt;br&gt;&quot;)
			Else
				objDoc.Write(objGroup.DisplayName &amp; &quot;: Update UNSUCCESSFUL.&lt;br&gt;&quot;)
			End If
		Else
			objDoc.Write(objGroup.DisplayName &amp; &quot;: Skipped (SmartDL).&lt;br&gt;&quot;)
		End If
	End If
	On Error Goto 0
 Next

 Set objOldUser = Nothing
 Set objTargetUser = Nothing
 Set objIE = Nothing
 Set objRecordset = Nothing
 Set objConnection = Nothing
 </pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/updated-copy-dls-from-one-user-to-another/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>How to add CAPTCHA to simpleblog 3.0</title>
		<link>http://www.flobee.net/how-to-add-captcha-to-simpleblog-3-0/</link>
		<comments>http://www.flobee.net/how-to-add-captcha-to-simpleblog-3-0/#comments</comments>
		<pubDate>Sun, 22 Jul 2007 04:34:30 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[VBScript]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=198</guid>
		<description><![CDATA[Johann almost added CAPTCHA verification to simpleblog 3, but he notes on his blog that he decided not to after looking at the pros and cons.&#160; He says that simpleblog isn&#8217;t a target of comment spam because of the inability to post html or javascript code into a comment.&#160; I disagree, however, because you still [...]]]></description>
			<content:encoded><![CDATA[<p>Johann almost added CAPTCHA verification to simpleblog 3, but he notes on his <a href="http://blog.8pixel.net" target="_blank">blog</a> that he decided not to after looking at the pros and cons.&nbsp; He says that simpleblog isn&#8217;t a target of comment spam because of the inability to post html or javascript code into a comment.&nbsp; I disagree, however, because you still get targeted by comment spam by the very nature that bots will still post comments.&nbsp; Even with approval enabled, I still have to delete the pending comments, and there can be A LOT of them.</p>
<p>Johann included a copy of Emir Tuzul&#8217;s free <a href="http://www.tipstricks.org" target="_blank">ASP CAPTCHA implementation</a>, but never incorporated it into simpleblog.&nbsp; I looked at how the code works and how Johann implemented comments, and I have successfully added CAPTCHA verification to the comments system.&nbsp; Since doing so a few days ago, not a single spam comment has been left.&nbsp; If you are interested, this is how to do it.</p>
<p>Since Johann included version 2 of the CAPTCHA code page, you do not need to download anything, but you can opt to use<a href="http://www.tipstricks.org/aspsig/examplev3.asp" target="_blank"> version 3 beta 1</a>, which uses more character obfuscation to make it harder for bots to determine the characters in the image.</p>
<p>Edit functions.asp to add the following code to the end of the file, which is the verification function:</p>
<pre class="brush:vb;gutter:false;wrap-lines:false;auto-links:false;collapse:false">
&lt;%
Function CheckCAPTCHA(valCAPTCHA)
	SessionCAPTCHA = Trim(Session(&quot;CAPTCHA&quot;))
	Session(&quot;CAPTCHA&quot;) = vbNullString
	If Len(SessionCAPTCHA) &lt; 1 Then
		CheckCAPTCHA = False
		Exit Function
	End if
	If CStr(SessionCAPTCHA) = CStr(valCAPTCHA) Then
		CheckCAPTCHA = True
	Else
		CheckCAPTCHA = False
	End if
End Function
%&gt;
</pre>
<p>Add the following code to functions.asp in the CommentsGet subroutine, which for me starts at line 351.&nbsp; It may be different for you since I think I have added other code higher in the file.&nbsp; This adds the actual CAPTCHA image to the comments form.&nbsp; You will add this code after the call for GetEmoticons and the line break, which for me means inserting this at line 454:</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:454">
Type the characters shown in the image for verification.
&lt;img src=&quot;captcha.asp&quot; alt=&quot;&quot; width=&quot;86&quot; height=&quot;21&quot; /&gt;
&lt;input name=&quot;strCAPTCHA&quot; type=&quot;text&quot; id=&quot;strCAPTCHA&quot; maxlength=&quot;8&quot; /&gt;&lt;/td&gt;
</pre>
<p>At line 481 (after the declaration of the str_userIP variable), insert this, which puts the characters entered into the form in a variable:</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:481">
strCAPTCHA = Trim(Request.Form(&quot;strCAPTCHA&quot;))
</pre>
<p>Lastly, replace the code that inserts the comment into the database with the code below, starting at line 492 (after the comment&nbsp; &quot;insert Comment.&quot;&nbsp; Instead of simply inserting the comment into the database, this will compare the entered characters to the actual ones in the image.&nbsp; If they match, the comment is inserted.&nbsp; If not, I use a JavaScript alert to present a popup box and then redirect the user back to the post:</p>
<pre class="brush:vb;gutter:true;wrap-lines:false;auto-links:false;collapse:false;first-line:492">
If CheckCAPTCHA(strCAPTCHA) = True Then
	SQL = &quot;INSERT INTO T_COMMENTS(c_content, c_name, c_email, c_url, c_bID_fk,ip) VALUES ('&quot; &amp; strComment &amp; &quot;','&quot; &amp; sanitize( strName ) &amp; &quot;','&quot; &amp; sanitize( strEmail ) &amp; &quot;','&quot; &amp;&nbsp; sanitize( strUrl )&amp; &quot;',&quot; &amp;&nbsp; sanitize( bID )&amp; &quot;,'&quot;&amp;str_userIP&amp;&quot;')&quot;
	Set MyConn = Server.CreateObject(&quot;ADODB.Connection&quot;)
	MyConn.Open strConn
	MyConn.Execute(strSQL)
	MyConn.Close
	Set MyConn = Nothing
	Response.Redirect(&quot;default.asp?view=plink&amp;id=&quot; &amp; bID &amp; &quot;&amp;comments=1&quot;)
Else
	%&gt;
	&lt;script language=&quot;Javascript&quot;&gt;
		alert('You did not type the verification code correctly.');
		location.replace('default.asp?view=plink&amp;id=&lt;%= bID %&gt;&amp;comments=1');
	&lt;/script&gt;
	&lt;%
End If
</pre>
<p>You&#8217;re done!&nbsp; Save functions.asp and then go add a comment to one of your posts.&nbsp; Intentionally enter incorrect characters to confirm the popup works and that the comment did not get added.&nbsp; The only thing missing from this is that it doesn&#8217;t preserve the comment in the session.&nbsp; This means that if a real person incorrectly enters the code, when returned to the post to try and enter another code, the actual comment data will have to be entered again.&nbsp; Name, email, and URL don&#8217;t have to because they are stored in a cookie on the client.&nbsp; Perhaps I will add that at a later time.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/how-to-add-captcha-to-simpleblog-3-0/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Quick and dirty script to copy DLs from one user to another</title>
		<link>http://www.flobee.net/quick-and-dirty-script-to-copy-dls-from-one-user-to-another/</link>
		<comments>http://www.flobee.net/quick-and-dirty-script-to-copy-dls-from-one-user-to-another/#comments</comments>
		<pubDate>Sun, 04 Feb 2007 23:07:35 +0000</pubDate>
		<dc:creator>Scott</dc:creator>
				<category><![CDATA[Exchange 2003]]></category>
		<category><![CDATA[Exchange 2007]]></category>
		<category><![CDATA[VBScript]]></category>
		<category><![CDATA[inline code]]></category>

		<guid isPermaLink="false">http://www.flobee.net/?p=187</guid>
		<description><![CDATA[Edit: The inline code in this post is not the latest version of the script. Get the latest version from the downloads page. It is not uncommon at my company to have to move a user from one domain to another for technical, logistical, or political reasons.  For another set of reasons, moving the user [...]]]></description>
			<content:encoded><![CDATA[<p><font color="red">Edit:  The inline code in this post is not the latest version of the script.  Get the latest version from the <a href="/downloads">downloads</a> page.</font></p>
<p>It is not uncommon at my company to have to move a user from one domain to another for technical, logistical, or political reasons.  For another set of reasons, moving the user account to the other domain is not done, instead manually creating a new one and associating the mailbox with the new account.</p>
<p>DL membership does not automatically get updated when this is done, so I have been doing it manually.  It has been on my to-do list for awhile to write a script to copy the DL membership from the old account to the new one.  So I threw this together this morning.  It lacks some of the nice extras my other scripts have (finding the user by logon name, email results, true logging) but it does work.</p>
<p>You have to edit the script to give it the variables for the old and new users&#8217; dn.  It will skip security groups (any group without an alias) and also groups whose Notes (info) attribute contains the word SmartDL.  We use Imanami&#8217;s SmartDL for automated DL membership when applicable.  Those will be automatically updated the next time each of their jobs run.</p>
<p>Download it <a title="Copy DLs from one user to another" href="http://www.flobee.net/download/CopyDLMembership.zip">here</a>, or copy/paste below.</p>
<pre class="brush:vb;gutter:false;wrap-lines:false;auto-links:false;collapse:true">
Option Explicit
Dim strOldUser, strNewUser, objOldUser, objNewUser, strGroup, objGroup
strOldUser = &quot;&quot; 'dn of user to copy from'
strNewUser = &quot;&quot; 'dn of user to copy to'
Set objOldUser = GetObject(&quot;LDAP://&quot; &amp; strOldUser)
Set objNewUser = GetObject(&quot;LDAP://&quot; &amp; strNewUser)
wscript.echo &quot;Source user: &quot; &amp; objOldUser.DisplayName
wscript.echo &quot;Target user: &quot; &amp; objNewUser.DisplayName
For Each strGroup in objOldUser.MemberOf
	On Error Resume Next
	Set objGroup = GetObject(&quot;LDAP://&quot; &amp; strGroup)
    If Not Trim(objGroup.mailNickname) = &quot;&quot; Then
        If Not Instr(objGroup.info, &quot;SmartDL&quot;) &gt; 0 Then
			objGroup.Add(objNewUser.ADsPath)
			If Err.Num = 0 Then
				wscript.echo objGroup.DisplayName &amp; &quot;: Update successful.&quot;
			Else
				wscript.echo objGroup.DisplayName &amp; &quot;: Update UNSUCCESSFUL.&quot;
			End If
        Else
			wscript.echo objGroup.DisplayName &amp; &quot;: Skipped (SmartDL).&quot;
        End If
	End If
	On Error Goto 0
Next
Set objOldUser = Nothing
Set objNewUser = Nothing
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.flobee.net/quick-and-dirty-script-to-copy-dls-from-one-user-to-another/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

