My journey from Windows CE to Android

After over 14 years using some kind of Microsoft-powered mobile device, I recently got a Google Nexus 4.  I thought I would look back at the devices I have used over the years.  Most have long been recycled, but a few of them are in a drawer (or perhaps even the garage).

Google Nexus 4 (LG E960)
Released: November, 2012
Google Nexus 4
AT&T Tilt2 (HTC Rhodium)
Released: October, 2009
AT&T Tilt 2
Samsung BlackJack (Samsung SGH-i607)
Released: November, 2006
Samsung BlackJack
Cingular 2125 (HTC Faraday)
Released: March, 2006
Cingular 2125
Cingular 8125 (HTC Wizard)
Released: January, 2006
Cingular 8125
Sprint PPC-6700 (HTC Apache)
Released: October, 2005
Sprint-PPC6700
Siemens SX66 (HTC Blue Angel)
Released: December, 2004
Siemens SX66
Audiovox SMT-5600 (HTC Typhoon)
Releases: November, 2004
Audiovox SMT5600
Audiovox PPC-4100
Released: June, 2004
Audiovox PPC-4100
Hitachi SH-G1000
Released: July, 2003
Hitachi G1000
Siemens SX56 (HTC Wallaby)
Released: October, 2002
Siemens SX56
Casio Cassiopeia E-100
Released: May, 1999
Casio E100
Philips Nino 300
Released: September, 1998
Philips Nino

Small update to delegate management module

Articles in the "Mailbox Delegate Management" series

  1. Super duper delegate retrieval script
  2. PowerShell module for managing Exchange 2010 mailbox delegates
  3. Small update to delegate management module [This article]

The Exchange 2010 delegate management module has been updated to v1.3.2. It includes a couple of small changes to resolve an issue if you use the module inside the Exchange Management Shell (as opposed to running vanilla PowerShell with implicit remoting). The inline code displayed in the main post has been updated, as has the downloadable copy.

  DelegateManagement.zip (5.7 KiB)

PowerShell module for managing Exchange 2010 mailbox delegates

Articles in the "Mailbox Delegate Management" series

  1. Super duper delegate retrieval script
  2. PowerShell module for managing Exchange 2010 mailbox delegates [This article]
  3. Small update to delegate management module

I have converted my script that displays a mailbox’s delegates into a module that adds the ability to add, modify, and remove a mailbox’s delegates. The benefit of the module is that it negates the need to create an Outlook profile for the owner/manager in order to manipulate his or her delegates. The module requires any version of the EWS Managed API and the Exchange cmdlets via remoting, as detailed below.

Upon importing the module, it will check for any version of the EWS Managed API, starting with version 2.0 and working its way back. This allows for the freedom to have any version installed instead of requiring everyone who uses it to ensure they have the specific one I used when writing it. After the API is loaded, it will check for the Exchange cmdlets. If not loaded into the shell, it will retrieve a list of Exchange servers from Active Directory and attempt to connect to one until successful. If neither the API nor remoting to Exchange is successful, the module will fail to load, telling you why. (The module doesn’t distinguish between the cmdlets being available because they were locally loaded from the snap-in or from remoting. However, since certain cmdlets will fail when not executed remotely because they bypass RBAC, you need to be sure that you have not locally loaded the snap-in.)

Access to a mailbox is done using impersonation by the person running a cmdlet. If you won’t be using impersonation, instead relying on full mailbox access, you will want to comment out line 94. The URL used for EWS is determined by autodiscover of the owner/manager mailbox.

These are the cmdlets the module exposes:

Get-MailboxDelegate

The alias for Get-MailboxDelegate is gmd.  It is basically the same as my Get-Delegates.ps1 script, but it has gotten a makeover to support pipelining into and out of. The -Identity parameter (aliased to -Owner and -Manager) is any standard Exchange identity (display name, email address, alias, dn, etc.) and it supports pipelining by property name. If the objects you are pipelining into Get-MailboxDelegate don’t have a property name of Identity, then you will need to use a ForEach loop and use $_.PropertyName to designate which property should be used.

Without any other parameters, all delegates will be retrieved. If you want to get only a specific delegate, you can use the -Delegate parameter. The default output will be to list the properties, but since it is now a collection of objects, you can choose to output it to a table, to a grid view, or export to a file, using the appropriate cmdlets. You can also use these output cmdlets to select only the properties you want. For example, if you only care about the private items property you could use ft owner,delegate,viewprivate. Or, if you only want those who actually can view private items, you could run something like this:

(Get-DistributionGroupMember <DLIdentity>) | gmd | where {$_.ViewPrivate -eq $true} | ft owner,delegate

Note that I encapsulated in parentheses the command that I pipeline into Get-MailboxDelegate. This is necessary to avoid the concurrent pipeline limitation in remote PowerShell. It is only necessary if the command prior to the pipeline will be running a cmdlet that leverages remoting. Another option is to store the results of the prior command in a variable and then pipeline that into Get-MailboxDelegate.

All of the module’s cmdlets have built-in help, so you can use PowerShell’s help cmdlet to learn the details of all of them, such as the parameters and their descriptions, usage examples, etc.

Add-MailboxDelegate

The alias for Add-MailboxDelegate is amd. To use this cmdlet, provide an owner and a delegate. You can optionally specify folder permission for Inbox, Calendar, Tasks, Contacts, Sent Items, and Deleted Items; if private items are viewable; if meeting requests are to be received; and the owner’s global handling of meeting requests. I didn’t include the option of setting permission for Journal or Notes because, well, who uses them? The ability to set the permission for Sent Items and Deleted Items is to accommodate those who use GPO to have Outlook store messages sent from another user in that person’s Sent Items folder, and likewise for messages deleted from another mailbox. The option to set the meeting request delivery scope applies to the owner, not the delegate being added, so it is only necessary to include it if you are adding a delegate and you want to change the current setting.

Set-MailboxDelegate

The alias for Set-MailboxDelegate is smd. Use this cmdlet to change any settings for an existing delegate (or to change the meeting request delivery scope for the owner). Provide the owner and the existing delegate to modify and, optionally, which setting you want to change. All other settings will remain as is. If you want to change just the meeting request delivery scope for the owner, specify any existing delegate, but not any other settings (except the delivery scope). Unlike the valid roles for folder permission with Add-MailboxDelegate, you can use None if you want to remove a folder permission. If you want to remove the ability to view private items or to not receive meeting requests, use -ViewPrivateItems:$false or -ReceiveMeetingRequests:$false, respectively. The colon is necessary because both parameters are switches, so their inclusion alone in the command means true, whereas to explicitly set them to false means using the syntax above. (The cmdlet checks if either switch is actually present in the command, so don’t be concerned that not including a switch implies that it should be false.)

Remove-MailboxDelegate

The alias for Remove-MailboxDelegate is rmd. Provide the owner and the delegate to remove. That’s it.

All of the cmdlets perform the expected error checking: the owner is a valid mailbox; any delegate to add, modify, or remove is a valid mailbox; adding a delegate when the delegate already exists; modifying or removing a delegate that is not an existing delegate; using a valid (albeit the limited subset exposed in the API) role for a folder permission; and using a valid meeting request delivery scope.

Download the module or view/copy the code below:

  DelegateManagement.zip (5.7 KiB)

#Exchange 2010 mailbox delegate management module
#v1.3.2 4/18/13

#Paths to EWS Managed API DLL
$ewsAPIPaths = "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll",
"C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll",
"C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll",
"C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"

#Test if any version of API is installed before loading module functions
foreach ($path in $ewsAPIPaths)
	{
	if (Test-Path $path)
		{
		Add-Type -Path $path
		$apiFound = $true
		break
		}
	}

if (-not($apiFound))
	{
	Write-Error "The Exchange Web Services Managed API is required to use this module." -Category NotInstalled
	break
	}

function Get-ExchangeServerName
	{
	$configNC = ([ADSI]"LDAP://RootDse").configurationNamingContext
	$search = New-Object DirectoryServices.DirectorySearcher([ADSI]"LDAP://$configNC")
	$search.Filter = "(&(objectClass=msExchExchangeServer)(versionNumber>=1937801568))"
	$search.PageSize=1000
	$search.PropertiesToLoad.Clear()
	[void] $search.PropertiesToLoad.Add("networkaddress")
	$search.FindAll()
	}

function Get-ServerFqdnFromNetworkAddress($server)
	{
   	$server.properties["networkaddress"] |
      Where-Object {$_.ToString().StartsWith("ncacn_ip_tcp")} | ForEach-Object {$_.ToString().SubString(13)}
	}

function Connect-ExchangeServer($server)
	{
	try
		{
		Import-PSSession (New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$server/powershell" -Name 'RemoteExchange') -AllowClobber -DisableNameChecking -ErrorAction Stop | Out-Null
		$true
		}
	catch
		{
		$false
		}
	}
	
function Connect-RemoteExchangeServer
	{
	$exchangeServers = Get-ExchangeServerName
	$serverFQDN = $exchangeServers | ForEach-Object {Get-ServerFqdnFromNetworkAddress $_}
	#Loop through servers until connection is successful
	for ($i = 0; $i -lt $serverFQDN.Length; $i++)
		{
		$attemptResult = Connect-ExchangeServer $serverFQDN[$i]
		if ($attemptResult)
			{break}
		}
	if ($attemptResult)
		{$true}
	else
		{$false}
	}

#Test for Exchange cmdlets before loading any module functions
$testcmd = Get-Command Get-Mailbox -ErrorAction SilentlyContinue
if (-not($testcmd))
	{
	$connectResult = Connect-RemoteExchangeServer
	if (-not($connectResult))
		{
		Write-Error "Unable to connect to any Exchange server." -Category ConnectionError
		break
		}
	}

#Region Helper Functions
function Connect-WebServices ($smtpAddress)
	{
	$exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2 
	$global:exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion) 
 	#Use autodiscover instead of hard-coded URL
	$exchangeService.AutodiscoverUrl($smtpAddress) 
	#Impersonate mailbox
	$exchangeService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $smtpAddress)
	New-Object Microsoft.Exchange.WebServices.Data.Mailbox($smtpAddress)
	}
 
function Find-Mailbox ($identity)
	{
	try 
		{
		Get-Mailbox $identity -ErrorAction Stop
		}
	catch
		{
		Write-Progress 'Done' -Completed -Status " "
		Write-Error "A mailbox cannot be found that matches the input string $identity." -ErrorAction Stop -Category ObjectNotFound
		}
	}

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

function Get-FMA($identity)
	{
	Get-MailboxPermission $identity | Where-Object {$_.IsInherited -eq $false -and $_.User -notlike 'S-1-5-21*'}
	}
	
function Get-SendAs($identity)
	{
	Get-ADPermission $identity | Where-Object {$_.IsInherited -eq $false -and $_.ExtendedRights -eq 'Send-As'}
	}

function Get-FolderPermission($mailbox,$folder)
	{
	Get-MailboxFolderPermission "$mailbox`:\$folder"
	}
	
function Set-FolderPermission($owner,$delegate,$folder,$role)
	{
	$folderPerm = Get-FolderPermission $owner.Identity $folder
	[array]$delegateFolderPerm = $folderPerm | Where-Object {$_.User -eq $delegate.DisplayName}
	#Run appropriate cmdlet based on delegate presence in ACL
	if ($delegateFolderPerm.Count -eq 1)
		{
		try
			{
			Set-MailboxFolderPermission -Identity "$($owner.Identity):\$folder" -User $delegate.Identity -AccessRights $role -ErrorAction Stop | Out-Null
			}
		catch
			{
			$false
			}
		}
	else
		{
		try
			{
			Add-MailboxFolderPermission -Identity "$($owner.Identity):\$folder" -User $delegate.Identity -AccessRights $role -ErrorAction Stop | Out-Null
			}
		catch
			{
			$false
			}
		}
	}

function Convert-StringPermissionToEnum($role)
	{
	switch ($role)
		{
		'Reviewer' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Reviewer}
		'Author' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Author}
		'Editor' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Editor}
		'None' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::None}
		}
	}

function Convert-StringDeliveryScopeToEnum($scope)
	{
	switch ($scope)
		{
		'DelegatesOnly' {[Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesOnly}
		'DelegatesAndOwner' {[Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesAndMe}
		'DelegatesAndInfoToOwner' {[Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesAndSendInformationToMe}
		}
	}
#EndRegion

#Region Main Functions
function Add-MailboxDelegate
	{
	<#
	.Synopsis
		Add a mailbox as a delegate of an owner's mailbox.
	.Description
		Add a mailbox delegate, optionally specifying permission to various folders,
		whether private items are viewable by the delegate, and if the delegate should receive
		meeting requests for the owner.
	.Parameter Owner
		Identity string of the user whose mailbox is to have the delegate.
	.Parameter Delegate
		Identity string of the user who is to be added to owner's mailbox.
	.Parameter InboxPermission
		Role to assign to the Inbox folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter CalendarPermission
		Role to assign to the Calendar folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter TasksPermission
		Role to assign to the Tasks folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter ContactsPermission
		Role to assign to the Contacts folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter SentItemsPermission
		Role to assign to the Sent Items folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter DeletedItemsPermission
		Role to assign to the Deleted Items folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter ViewPrivateItems
		Enable the delegate to view items marked as private.
	.Parameter ReceiveMeetingRequests
		Enable the delegate to receive meeting requests for the owner.
	.Parameter MeetingRequestDeliveryScope
		Specify how meeting requests should be handled for the owner.  Valid scopes are DelegatesOnly,
		DelegatesAndOwner, and DelegatesAndInfoToOwner.  Note that this parameter applies to all delegates.
	.Example
		Add-MailboxDelegate Username DelegateUsername -InboxPermission Editor -CalendarPermission Editor -ViewPrivateItems
	.Example
		Add-MailboxDelegate -Owner domain\username -Delegate  -CalendarPermission Editor -ReceiveMeetingRequests
	.Notes
		Version: 1.01
		Date: 3/5/13
	#>
	[CmdletBinding()]
	param (
		[Parameter(Position=0,Mandatory=$true)][Alias("Manager")][string]$Owner,
		[Parameter(Position=1,Mandatory=$true)][string]$Delegate,
		[ValidateSet('Reviewer','Author','Editor')][string]$InboxPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$CalendarPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$TasksPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$ContactsPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$SentItemsPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$DeletedItemsPermission,
		[Alias("PI")][switch]$ViewPrivateItems,
		[Alias("MR")][switch]$ReceiveMeetingRequests,
		[ValidateSet('DelegatesOnly','DelegatesAndOwner','DelegatesAndInfoToOwner')][string]$MeetingRequestDeliveryScope
		)

	#Validate mailboxes
	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Validating owner and delegate mailboxes" -PercentComplete 0
	$mbOwner = Find-Mailbox $Owner
	$mbDelegate = Find-Mailbox $Delegate
	
	$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
	$ownerLastName = (Get-User $mbOwner.Identity).LastName
	$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
	$delegateLastName = (Get-User $mbDelegate.Identity).LastName

	#Get EWS mailbox reference
	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 25
	$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()
	
	#Get collection of delegates, without folder permissions
	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Retrieving existing delegates" -PercentComplete 50
	$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$false)
	
	#Check if user is already a delegate
	if ($currentDelegates.DelegateUserResponses.Count -gt 0)
		{
		foreach ($currentDelegate in $currentDelegates.DelegateUserResponses)
			{
			if ($currentDelegate.DelegateUser.UserId.PrimarySMTPAddress -eq $mbDelegate.PrimarySMTPAddress.ToString())
				{
				Write-Progress -Activity "Adding Mailbox Delegate" -Completed -Status " "
				Write-Host "$delegateFirstName $delegateLastName is already a delegate of $ownerFirstName $ownerLastName. Use Set-MailboxDelegate to update an existing delegate."
				return
				}
			}
		}

	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Adding delegate" -PercentComplete 75
	#Create EWS delegate object
	$delegateUser = New-Object Microsoft.Exchange.WebServices.Data.DelegateUser($mbDelegate.PrimarySMTPAddress.ToString())
	
	#Set private items
	if ($ViewPrivateItems)
		{
		$delegateUser.ViewPrivateItems = $true
		}
	
	#Set meeting request receipt
	if ($ReceiveMeetingRequests)
		{
		$delegateUser.ReceiveCopiesOfMeetingMessages = $true
		}
	
	#Set permissions on folders
	if ($InboxPermission)
		{
		$delegateUser.Permissions.InboxFolderPermissionLevel = Convert-StringPermissionToEnum $InboxPermission
		}
	if ($CalendarPermission)
		{
		$delegateUser.Permissions.CalendarFolderPermissionLevel = Convert-StringPermissionToEnum $CalendarPermission
		}
	if ($TasksPermission)
		{
		$delegateUser.Permissions.TasksFolderPermissionLevel = Convert-StringPermissionToEnum $TasksPermission
		}
	if ($ContactsPermission)
		{
		$delegateUser.Permissions.ContactsFolderPermissionLevel = Convert-StringPermissionToEnum $ContactsPermission
		}
	if ($SentItemsPermission)
		{
		$SIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Sent Items' $SentItemsPermission
		}
	if ($DeletedItemsPermission)
		{
		$DIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Deleted Items' $DeletedItemsPermission
		}
		
	#Build delegate collection object to use in EWS method
	$delegateArray = New-Object Microsoft.Exchange.WebServices.Data.DelegateUser[] 1 
	$delegateArray[0] = $delegateUser

	#Set new meeting request delivery scope if specified
	if ($MeetingRequestDeliveryScope)
		{
		$addResponse = $exchangeService.AddDelegates($EWSMailbox, (Convert-StringDeliveryScopeToEnum $MeetingRequestDeliveryScope), $delegateArray)
		}
	else
		{
		$addResponse = $exchangeService.AddDelegates($EWSMailbox, $null, $delegateArray)
		}
		
	Write-Progress -Activity "Adding Mailbox Delegate" -Completed -Status " "
	if ($addResponse[0].Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
		{
		Write-Host "$delegateFirstName $delegateLastName has been added as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Green
		if ($SIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Sent Items folder." -ForegroundColor Yellow
			}
		if ($DIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Deleted Items folder." -ForegroundColor Yellow
			}
		}
	else
		{
		Write-Host "An error occurred adding $delegateFirstName $delegateLastName as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Red
		}
	}

function Set-MailboxDelegate
	{
	<#
	.Synopsis
		Update the settings for an existing delegate of an owner's mailbox.
	.Description
		Update an existing mailbox delegate, specifying any changes to folder permissions,
		whether private items are viewable by the delegate, or if the delegate should receive
		meeting requests for the owner.
	.Parameter Owner
		Identity string of the user whose mailbox has the delegate.
	.Parameter Delegate
		Identity string of the user whose delegate settings are to be updated.
	.Parameter InboxPermission
		Role to assign to the Inbox folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter CalendarPermission
		Role to assign to the Calendar folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter TasksPermission
		Role to assign to the Tasks folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter ContactsPermission
		Role to assign to the Contacts folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter SentItemsPermission
		Role to assign to the Sent Items folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter DeletedItemsPermission
		Role to assign to the Deleted Items folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter ViewPrivateItems
		Enable the delegate to view items marked as private.
	.Parameter ReceiveMeetingRequests
		Enable the delegate to receive meeting requests for the owner.
	.Parameter MeetingRequestDeliveryScope
		Specify how meeting requests should be handled for the owner.  Valid scopes are DelegatesOnly,
		DelegatesAndOwner, and DelegatesAndInfoToOwner.  Note that this parameter applies to all delegates.
	.Example
		Set-MailboxDelegate Username DelegateUsername -InboxPermission Editor -CalendarPermission Editor -ViewPrivateItems
	.Example
		Set-MailboxDelegate -Owner domain\username -Delegate  -CalendarPermission Editor -ReceiveMeetingRequests
	.Notes
		Version: 1.01
		Date: 3/5/13
	#>
	[CmdletBinding()]
	param (
		[Parameter(Position=0,Mandatory=$true)][Alias("Manager")][string]$Owner,
		[Parameter(Position=1,Mandatory=$true)][string]$Delegate,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$InboxPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$CalendarPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$TasksPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$ContactsPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$SentItemsPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$DeletedItemsPermission,
		[Alias("PI")][switch]$ViewPrivateItems,
		[Alias("MR")][switch]$ReceiveMeetingRequests,
		[ValidateSet('DelegatesOnly','DelegatesAndOwner','DelegatesAndInfoToOwner')][string]$MeetingRequestDeliveryScope
		)

	#Validate mailboxes
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Validating owner and delegate mailboxes" -PercentComplete 0
	$mbOwner = Find-Mailbox $Owner
	$mbDelegate = Find-Mailbox $Delegate
	
	$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
	$ownerLastName = (Get-User $mbOwner.Identity).LastName
	$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
	$delegateLastName = (Get-User $mbDelegate.Identity).LastName
	
	#Get EWS mailbox reference
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 25
	$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()
	
	#Get collection of delegates, with folder permissions
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Retrieving existing delegates" -PercentComplete 50
	$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$true)
	
	#Confirm user is already a delegate
	$delegateMatch = $false
	if ($currentDelegates.DelegateUserResponses.Count -eq 0)
		{
		Write-Progress -Activity "Updating Mailbox Delegate" -Completed -Status " "
		Write-Host "$ownerFirstName $ownerLastName does not have any delegates. `
				Use Add-MailboxDelegate to add a new delegate."
				return		
		}
	elseif ($currentDelegates.DelegateUserResponses.Count -gt 0)
		{
		foreach ($currentDelegate in $currentDelegates.DelegateUserResponses)
			{
			if ($currentDelegate.DelegateUser.UserId.PrimarySMTPAddress -eq $mbDelegate.PrimarySMTPAddress.ToString())
				{
				#Modify existing delegate object instead of creating new one so existing settings
				#can be preserved
				$delegateUser = $currentDelegate.DelegateUser
				$delegateMatch = $true
				}
			}
		if (-not($delegateMatch))
			{
			Write-Progress -Activity "Updating Mailbox Delegate" -Completed -Status " "
			Write-Host "$delegateFirstName $delegateLastName is not a delegate of $ownerFirstName $ownerLastName. Use Add-MailboxDelegate to add a new delegate."
			return
			}
		}
	
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Updating delegate" -PercentComplete 75
	
	#Set private items if included
	if ($MyInvocation.BoundParameters.ContainsKey('ViewPrivateItems'))
		{
		$delegateUser.ViewPrivateItems = $ViewPrivateItems
		}
	
	#Set meeting request receipt if included
	if ($MyInvocation.BoundParameters.ContainsKey('ReceiveMeetingRequests'))
		{
		$delegateUser.ReceiveCopiesOfMeetingMessages = $ReceiveMeetingRequests
		}
	
	#Set permissions on folders
	if ($InboxPermission)
		{
		$delegateUser.Permissions.InboxFolderPermissionLevel = Convert-StringPermissionToEnum $InboxPermission
		}
	if ($CalendarPermission)
		{
		$delegateUser.Permissions.CalendarFolderPermissionLevel = Convert-StringPermissionToEnum $CalendarPermission
		}
	if ($TasksPermission)
		{
		$delegateUser.Permissions.TasksFolderPermissionLevel = Convert-StringPermissionToEnum $TasksPermission
		}
	if ($ContactsPermission)
		{
		$delegateUser.Permissions.ContactsFolderPermissionLevel = Convert-StringPermissionToEnum $ContactsPermission
		}
	if ($SentItemsPermission)
		{
		$SIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Sent Items' $SentItemsPermission
		}
	if ($DeletedItemsPermission)
		{
		$DIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Deleted Items' $DeletedItemsPermission
		}
		
	#Build delegate collection object to use in EWS method
	$delegateArray = New-Object Microsoft.Exchange.WebServices.Data.DelegateUser[] 1 
	$delegateArray[0] = $delegateUser

	#Set new meeting request delivery scope if specified
	if ($MeetingRequestDeliveryScope)
		{
		$updateResponse = $exchangeService.UpdateDelegates($EWSMailbox, (Convert-StringDeliveryScopeToEnum $MeetingRequestDeliveryScope), $delegateArray)
		}
	else
		{
		$updateResponse = $exchangeService.UpdateDelegates($EWSMailbox, $null, $delegateArray)
		}
		
	Write-Progress -Activity "Updating Mailbox Delegate" -Completed -Status " "
	if ($updateResponse[0].Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
		{
		Write-Host "$delegateFirstName $delegateLastName has been updated as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Green
		if ($SIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Sent Items folder." -ForegroundColor Yellow
			}
		if ($DIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Deleted Items folder." -ForegroundColor Yellow
			}
		}
	else
		{
		Write-Host "An error occurred updating $delegateFirstName $delegateLastName as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Red
		}
	}
	
function Remove-MailboxDelegate
	{
	<#
	.Synopsis
		Remove a delegate from an owner's mailbox.
	.Description
		Remove a supplied mailbox delegate from a supplied owner's mailbox.
	.Parameter Owner
		Identity string of the user whose mailbox has the delegate.
	.Parameter Delegate
		Identity string of the user who is to be removed from the owner's mailbox.
	.Example
		Remove-MailboxDelegate user@domain.com delegate@domain.com
	.Example
		Remove-MailboxDelegate -Owner domain\username -Delegate 
	.Notes
		Version: 1.01
		Date: 3/5/13
	#>
	[CmdletBinding()]
	param (
		[Parameter(Position=0,Mandatory=$true)][Alias("Manager")][string]$Owner,
		[Parameter(Position=1,Mandatory=$true)][string]$Delegate
		)

	#Validate mailboxes
	Write-Progress -Activity "Removing Mailbox Delegate" -Status "Validating owner and delegate mailboxes" -PercentComplete 0
	$mbOwner = Find-Mailbox $Owner
	$mbDelegate = Find-Mailbox $Delegate

	$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
	$ownerLastName = (Get-User $mbOwner.Identity).LastName
	$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
	$delegateLastName = (Get-User $mbDelegate.Identity).LastName
	
	#Get EWS mailbox reference
	Write-Progress -Activity "Removing Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 25
	$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()
	
	#Get collection of delegates, without folder permissions
	Write-Progress -Activity "Removing Mailbox Delegate" -Status "Retrieving delegates" -PercentComplete 50
	$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$false)
	if ($currentDelegates.DelegateUserResponses.Count -eq 0)
		{
		Write-Progress -Activity "Removing Mailbox Delegate" -Completed -Status " "
		Write-Host $ownerFirstName $ownerLastName "does not have any delegates."
		}
	else
		{
		$delegateToRemove = @()
		$delegateMatch = $false
		Write-Progress -Activity "Removing Mailbox Delegate" -Status "Removing delegate" -PercentComplete 75
		foreach ($currentDelegate in $currentDelegates.DelegateUserResponses)
			{
			if ($currentDelegate.DelegateUser.UserId.PrimarySMTPAddress -eq $mbDelegate.PrimarySMTPAddress.ToString())
				{
				#Add userid object to collection of delegates to remove
				$delegateToRemove += $currentDelegate.DelegateUser.UserId
				$delegateMatch = $true
				}
			}	
		if (-not($delegateMatch))
			{
			Write-Progress -Activity "Removing Mailbox Delegate" -Completed -Status " "
			Write-Host $mbDelegate.PrimarySMTPAddress "is not a delegate of" $mbOwner.PrimarySMTPAddress "."
			}
		else
			{
			#Remove delegate from owner's mailbox
			$removeResponse = $exchangeService.RemoveDelegates($EWSMailbox, $delegateToRemove)
			Write-Progress -Activity "Removing Mailbox Delegate" -Completed -Status " "
			if ($removeResponse[0].Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
				{
				Write-Host "$delegateFirstName $delegateLastName has been removed as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Green
				}
			else
				{
				Write-Host "An error occurred removing $delegateFirstName $delegateLastName as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Red
				}
			}
		}
	}

function Get-MailboxDelegate
	{
	<#
	.Synopsis
		Display a mailbox's delegates and permissions.
	.Description
		Retrieve the list of delegates for a mailbox and display the mailbox permission,
		folder permissions, meeting invite settings, and (optionally) whether the
		delegate has Send As permission.
	.Parameter Identity
		Identity string of the user whose mailbox has the delegates.  Owner and Manager are valid
		aliases for this parameter.
	.Parameter Delegate
		Identity string of the delegate you want to retrieve.  If omitted, all delegates
		are retrieved.
	.Parameter IncludeSendAs
		Switch to indicate that you want Send As permission to be included.
	.Example
		Get-MailboxDelegate user@domain.com -includesendas
	.Example
		Get-MailboxDelegate domain\username
	.Notes
		Version: 1.7
		Date: 3/12/13
	#>
	param (
		[Parameter(Position=0,Mandatory=$true,HelpMessage="Identity of mailbox owner",ValueFromPipelineByPropertyName=$true)]
		[Alias("Owner")][Alias("Manager")][string]$Identity,
		[Parameter(Position=1)][string]$Delegate,
		[Alias("SA")][switch]$includeSendAs #Perform Send As lookup (takes longer)	
		)
	
	process
		{
		#Validate owner mailbox
		Write-Progress -Activity "Getting Mailbox Delegate" -Status "Validating owner mailbox" -PercentComplete 0
		$mbOwner = Find-Mailbox $Identity
		
		#Validate delegate mailbox
		if ($Delegate)
			{
			Write-Progress -Activity "Getting Mailbox Delegate" -Status "Validating delegate mailbox" -PercentComplete 5
			$mbDelegate = Find-Mailbox $Delegate
			$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
			$delegateLastName = (Get-User $mbDelegate.Identity).LastName
			}
		
		$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
		$ownerLastName = (Get-User $mbOwner.Identity).LastName

		#Get EWS mailbox reference
		Write-Progress -Activity "Getting Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 10
		$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()

		#Get list of delegates and permissions from EWS
		Write-Progress -Activity "Getting Mailbox Delegate" -Status "Retrieving delegates" -PercentComplete 20
		#Get collection of delegates, with folder permissions
		if ($mbDelegate)
			{
			#Retrieve only the specified delegate
			$delegateUser = New-Object Microsoft.Exchange.WebServices.Data.UserId($mbDelegate.PrimarySMTPAddress.ToString())
			$delegateArray = New-Object Microsoft.Exchange.WebServices.Data.UserId[] 1 
			$delegateArray[0] = $delegateUser
			$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$true,$delegateArray)
			}
		else
			{
			$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$true)
			}

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

		#Build output
		$outputArr = New-Object System.Collections.ArrayList
		#Loop through list of delegates
		if ($currentDelegates.DelegateUserResponses.Count -gt 0)
			{
			if ($mbDelegate -and $currentDelegates.DelegateUserResponses[0].ErrorCode -eq 'ErrorNotDelegate')
				{
				Write-Host "$delegateFirstName $delegateLastName is not a delegate of $ownerFirstName $ownerLastName."
				}
			else
				{
				$currentDelegates.DelegateUserResponses | ForEach-Object {
					#Create custom object with property names
					$outputItem = "" | Select-Object Owner,Delegate,MeetingHandling,FMA,SendAs,Calendar,Inbox,Contacts,Tasks,DeletedItems,SentItems,ReceiveMeetings,ViewPrivate,Error,ErrorNote
					$outputItem.Owner = $mbOwner.DisplayName
					$outputItem.MeetingHandling = $currentDelegates.MeetingRequestsDeliveryScope
					if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
						{#Delegate account deleted in AD but still listed in list
						$outputItem.Error = "Orphan"
						$outputItem.ErrorNote = "Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml property 0x684A101E to determine orphan entry."
						}
					elseif ($_.ErrorMessage -eq 'Delegate is not configured properly.')
						{
						$outputItem.Error = "Misconfigured"
						$outputItem.ErrorNote = "Missing from Freebusy Data folder or publicDelegates attribute."
						}
					elseif ($_.Result -eq 'Error')
						{
						$outputItem.Error = "UnknownError"
						$outputItem.ErrorNote = $_.ErrorMessage
						}
					else
						{
						$delegateDisplayName = $_.delegateuser.userid.displayname
						$outputItem.Delegate = $delegateDisplayName
						if ($fmaSID -match $_.delegateuser.UserId.SID)
							{
							$outputItem.FMA = $true
							}
						else
							{
							$outputItem.FMA = $false
							}
						if ($includeSendAs)
							{
							if ($saSID -match $_.delegateuser.UserId.SID)
								{
								$outputItem.SendAs = $true
								}
							else
								{
								$outputItem.SendAs = $false
								}
							}
						$outputItem.Calendar = $_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString()
						$outputItem.Inbox = $_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString()
						$outputItem.Contacts = $_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString()
						$outputItem.Tasks = $_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString()
						
						#Construct Deleted Items permission output
						[array]$delegateDIPerm = $deletedItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
						if ($delegateDIPerm.Count -eq 1)
							{
							$delegateDIPermValue = $delegateDIPerm[0].AccessRights[0].ToString()
							}
						else
							{
							$delegateDIPermValue = 'None'
							}
						$outputItem.DeletedItems = $delegateDIPermValue
						
						#Construct Sent Items permission output
						[array]$delegateSIPerm = $sentItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
						if ($delegateSIPerm.Count -eq 1)
							{
							$delegateSIPermValue = $delegateSIPerm[0].AccessRights[0].ToString()
							}
						else
							{
							$delegateSIPermValue = 'None'
							}
						$outputItem.SentItems = $delegateSIPermValue
						
						$outputItem.ReceiveMeetings = $_.delegateuser.receivecopiesofmeetingmessages
						$outputItem.ViewPrivate = $_.delegateuser.viewprivateitems
						
						$outputArr.Add($outputItem) | Out-Null
						}
					}
				}
			}
		else
			{
			Write-Host "$ownerFirstName $ownerLastName has no delegates."
			}
		$outputArr
		}
	}
#EndRegion

Set-Alias amd Add-MailboxDelegate
Set-Alias smd Set-MailboxDelegate
Set-Alias rmd Remove-MailboxDelegate
Set-Alias gmd Get-MailboxDelegate
Export-ModuleMember -Function *-MailboxDelegate
Export-ModuleMember -Alias *

Update to Lync call handling script

Articles in the "Lync call handling script" series

  1. Trigger something whenever you’re on a Lync call
  2. Update to Lync call handling script [This article]

Yesterday I discovered that my Lync call handling script breaks when the Lync client is closed. I thought I had tested it and found that the client didn’t actually need to be running for the Lync client class object to be created and used. Not only was I wrong about that, but the existing client class object becomes invalid when Lync is closed. I suppose it isn’t that big of a deal to say that you need to rerun the script if you close Lync (and not run it until after Lync has been started), but I took it as a challenge to overcome that need.

I need to know both when Lync has started and when it has been closed. I started with the ClientState enumeration used in the LyncClient class. Since I already have an event for the client state, I figured I could act when the ShuttingDown state occurs. Uh, no, since that state never happens when Lync is running as an interactive process, but only when you are programmatically running Lync without a UI. The client state becomes Invalid when Lync is closed, so then I thought I could act when that state occurs. Also no, since an event doesn’t fire when the state becomes Invalid. I am now forced to look outside of the Microsoft.Lync.Model namespace.

What I decided to do is monitor the Lync process itself. I moved the creation of the client object into a function that can be called as needed, instead of just the one time when the script is run.

function Connect-LyncClient
	{
	$i = 1
	do
		{
		#Attach to Lync process, if running
		$lyncProcess = [System.Diagnostics.Process]::GetProcessesByName('communicator')
		if ($lyncProcess.Length -eq 0) #Process is not running
			{
			if ($i -eq 1) #Report status only on first attempt
				{
				Write-Host "$((Get-Date).ToLongTimeString()) - Waiting for Lync process to start (15-second intervals)..." -ForegroundColor Yellow
				}
			$i++
			Start-Sleep 15
			}
		}
	until ($lyncProcess.Length -eq 1)

	#Register for when Lync process exits
	Register-ObjectEvent -InputObject $lyncProcess[0] -EventName "Exited" -SourceIdentifier "LyncProcessHandler" -Action {LyncProcess-Handler} | Out-Null

	#Wait for client object initialization to complete
	do
		{
		$global:client = [Microsoft.Lync.Model.LyncClient]::GetClient()
		}
	while ($client.State -eq [Microsoft.Lync.Model.ClientState]::Invalid)
	}

The function checks to see if the communicator.exe process is running. If not, it will check every 15 seconds to see if it is. When the process has started, an event is registered that will fire when the process has exited. Now that the process has started, the client can be created. The LyncProcess-Handler function that is called by the event action unregisters the script events and then calls the above function to start the monitoring of the Lync process again. This way the script only has to be run once and it will accommodate whether Lync isn’t running in the first place and if it closes later.

The original code that creates the initial event registrations for contact activity and client state have also been moved into a function so it can be called again whenever the Lync process is started.

The script has been updated (available below), as well as the full version that is included in the body of the first post.

  Monitor-LyncSelfActivityChange.zip (640.3 KiB)

Trigger something whenever you’re on a Lync call

Articles in the "Lync call handling script" series

  1. Trigger something whenever you’re on a Lync call [This article]
  2. Update to Lync call handling script

A coworker of mine works out of his home office and wanted his family to know when he is on a phone call so they won’t interrupt him. He already had a network-controllable light that he put into the hallway outside the office. What he needed was a way to trigger the light when he is on the phone (he is enterprise voice-enabled in Lync). Here are the details of the PowerShell script that I wrote for him.

The script leverages the API in the Lync SDK. You only need to install the Lync SDK runtime library, but there isn’t an individual download for that. To install the Lync SDK, however, requires that Visual Studio is installed. That may not be the case for many of you, so I have included the redistributable runtime library (a mere 750KB MSI) in the script download.

The runtime library installs the assemblies into the global access cache (GAC), so to use one of them in a script, you need to load the assembly:

$apiPath = "C:\Windows\assembly\GAC_MSIL\Microsoft.Lync.Model\4.0.0.0__31bf3856ad364e35\Microsoft.Lync.Model.dll"
Add-Type -Path $apiPath

You can then create an object for the Lync client:

$client = [Microsoft.Lync.Model.LyncClient]::GetClient()

In order to know someone’s current presence state (or activity in Lync parlance), you work against a contact object. Therefore, to know your own activity, you first need to create an object for yourself as a contact:

$selfContact = $client.Self.Contact

To get your current activity, you use the GetContactInformation() method, with the argument being the type of information to retrieve:

$selfContact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType]::Activity)

The activities that indicate you are on the phone, or off-hook in telecom parlance, are In a call and In a conference call. To use this information to do something, you need to know when Lync changes to and/or from these activities, and for that you can use .NET Framework object events. Instead of constantly querying for your activity, you can use events to have PowerShell act when something happens. The Contact class has an event for when contact information changes, and here is the list of what properties will result in that event firing.

To register for an event, you have to include the object whose class contains the event, as well what action to take:

Register-ObjectEvent -InputObject $selfContact -EventName "ContactInformationChanged" -SourceIdentifier "OffHookHandler" -Action {Offhook-Handler $event}

$event is one of the built-in variables that the event can include in the response. And this is a truncated version of the function that will run as a result of the event firing:

function Offhook-Handler ($event)
	{
	#Act if what has changed is activity
	if ($event.SourceEventArgs.ChangedContactInformation -contains 'Activity')
		{
		$activity = $selfContact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType]::Activity)
		#Act if off- or on-hook
		if ($activity -eq 'In a call' -or $activity -eq 'In a conference call')
			{
			Write-VerboseEvent $activity
			if ($offhook -eq $false) #Only run off-hook action if not already on a call
				{
				OffHook-Action 'offhook'
				$global:offhook = $true #Stateful tracking of status in successive changes
				}
			}
		else
			{
			if ($offhook)
				{
				OffHook-Action 'onhook'
				Write-VerboseEvent "No longer on the phone"
				$global:offhook = $false
				}
			else
				{
				Write-VerboseEvent "Non-phone activity change: $activity"
				}
			}
		}
	}

Since the list of changed properties that triggers a changed contact event is numerous, the first thing the function does is check to see if the changed property is Activity. The event doesn’t include what the activity is, so you have to get the current activity. If the activity is one of the values that indicates you are on the phone, it calls a function to run your code that does something. (I did it this way, instead of having you put your code inside this function, so that it is easier to paste your code into a function at the beginning of the script and for doing so if there are future versions of the script.) A global variable of $offhook is set. This allows the script to keep track of whether you are off the phone, after having been on the phone, in subsequent event firings. If you are no longer on the phone, after having been on the phone, it calls another function to run your code that “undoes” whatever it did when you got on the phone.

The script will normally not output anything to the screen, allowing you to run it in a shell you are using for something else. If you are testing your code or just want to see more information about your activity changes, you can include the -Verbose parameter. This will result in more details being output to the screen as they occur, such as current activity, functions being called, and whether the client is signed in. If you do this, you will likely want to run the script in its own shell so it doesn’t get in the way of what you might be doing in the first shell.

Now, to put all of this into a reusable script, there are other things to consider. If you sign out, or get otherwise disconnected from Lync, the contact object is no longer valid and has to be created again when you sign in. To account for this, I use another event that fires on client state changes. If not signed in, it unregisters the contact change event, and when you get signed in again it creates the contact object and registers the event.

Another consideration is the scope of the script. When you run a script, its code is run in its own scope, terminating when the script reaches the end. Since the registered events will call functions after the script has completed, they need to be run in a scope that stays resident upon completion, i.e., global scope. To do this, you dot-source the script by entering a period and a space before the path to the script. To account for this, the script checks to ensure that it was dot-sourced when executed, otherwise it presents an error.

As for using the -Verbose parameter, I found that it worked fine when using Write-Verbose in the script’s main block and initial functions. It did not work in the functions called by the events. The function would just hang at the point of running the cmdlet. I have not been able to find out why this is the case, so I wrote a function to mimic the output of the Write-Verbose cmdlet. This function is used in the functions called by the events.

To make use of this script in your environment, you will want to modify the first function, called OffHook-Action, by replacing lines 13 and 19 with code to “do” something and “undo” something, respectively. If those lines of code are dependent on other functions, you can put them in the subsequent region that is labeled for your dependent functions.

As a side note, since I don’t have a network-controllable light at work, I needed something else for it to do so I could be sure it was running correctly. I use Winamp, so it was appropriate for me to have it pause when I am on the phone, and resume if I am off the phone (but only if it is already paused). The code in the script, therefore, includes functions for controlling Winamp and reference to a COM object the provides this. I left it in so you can see an example of the custom code that you will need to put in your version. It is worth nothing that the COM object for controlling Winamp is a 32-bit library. If you are running PowerShell on a 64-bit system and reference a library that is only compiled for a 32-bit system, you will need to run the x86 version of PowerShell (there is a Start Menu item for it). (The Lync SDK runtime is accessible in both versions of the shell.)

The download of the full version of the script (and the Lync SDK runtime library) is below, but you can also see the full script by expanding the code block below:

  Monitor-LyncSelfActivityChange.zip (640.3 KiB)

#Take an action when Lync presence indicates you are on a phone call
#v1.4 1/25/13

[CmdletBinding()]
param()

#Put your custom code in this function in response to being on or off the phone
function OffHook-Action ($action)
	{
	if ($action -eq 'offhook')
		{
		#Do something
		Pause-Winamp
		Write-VerboseEvent "Off-hook action executed"
		}
	else
		{
		#Undo something
		Play-Winamp
		Write-VerboseEvent "On-hook action executed"
		}
	}

#Region Your dependent function(s) for off-hook actions
$winamp = New-Object -ComObject ActiveWinamp.Application

function Play-Winamp
	{
	#Play only if Winamp currently paused
	if ($winamp.PlayState -eq 3)
		{
		$winamp.Play()
		}
	}
	
function Pause-Winamp
	{
	#Pause only if Winamp currently playing
	if ($winamp.PlayState -eq 1)
		{
		$winamp.Pause()
		}
	}
#EndRegion	

#Region Core functions
function Write-VerboseEvent ($text)
	{
	if ($verboseEvent)
		{
		Write-Host "VERBOSE: $((Get-Date).ToLongTimeString()) - $text" -ForegroundColor Yellow -BackgroundColor Black
		}
	}

function Connect-LyncClient
	{
	$i = 1
	do
		{
		#Attach to Lync process, if running
		$lyncProcess = [System.Diagnostics.Process]::GetProcessesByName('communicator')
		if ($lyncProcess.Length -eq 0) #Process is not running
			{
			if ($i -eq 1) #Report status only on first attempt
				{
				Write-Host "$((Get-Date).ToLongTimeString()) - Waiting for Lync process to start (15-second intervals)..." -ForegroundColor Yellow
				}
			$i++
			Start-Sleep 15
			}
		}
	until ($lyncProcess.Length -eq 1)

	#Register for when Lync process exits
	Register-ObjectEvent -InputObject $lyncProcess[0] -EventName "Exited" -SourceIdentifier "LyncProcessHandler" -Action {LyncProcess-Handler} | Out-Null

	#Wait for client object initialization to complete
	do
		{
		$global:client = [Microsoft.Lync.Model.LyncClient]::GetClient()
		}
	while ($client.State -eq [Microsoft.Lync.Model.ClientState]::Invalid)
	}

function Register-ContactChange
	{
	#Create self object as a contact
	$global:selfContact = $client.Self.Contact
	#Register for contact changes
	Register-ObjectEvent -InputObject $selfContact -EventName "ContactInformationChanged" -SourceIdentifier "OffHookHandler" -Action {Offhook-Handler $event} | Out-Null
	}

function Register-ClientStateChange
	{
	#Register for sign-in changes
	Register-ObjectEvent -InputObject $client -EventName "StateChanged" -SourceIdentifier "ClientStateHandler" -Action {ClientState-Handler $event} | Out-Null
	}

function Offhook-Handler ($event)
	{
	#Act if what has changed is activity
	if ($event.SourceEventArgs.ChangedContactInformation -contains 'Activity')
		{
		$activity = $selfContact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType]::Activity)
		#Act if off- or on-hook
		if ($activity -eq 'In a call' -or $activity -eq 'In a conference call')
			{
			Write-VerboseEvent $activity
			if ($offhook -eq $false) #Only run off-hook action if not already on a call
				{
				OffHook-Action 'offhook'
				$global:offhook = $true #Stateful tracking of status in successive changes
				}
			}
		else
			{
			if ($offhook)
				{
				OffHook-Action 'onhook'
				Write-VerboseEvent "No longer on the phone"
				$global:offhook = $false
				}
			else
				{
				Write-VerboseEvent "Non-phone activity change: $activity"
				}
			}
		}
	}
	
function ClientState-Handler ($event)
	{
	#Get current client state
	$newState = $event.SourceEventArgs.NewState
	if ($newState -eq 'SignedIn')
		{
		Register-ContactChange
		Write-VerboseEvent "Activity changes now being monitored."
		}
	else 
		{
		$subscriptionSource = Get-EventSubscriber | Select-Object -ExpandProperty SourceIdentifier
		if ($subscriptionSource -contains "OffHookHandler")
			{
			#If subscription currently is registered, remove it so it can
			#be successfully created again when signed in
			Unregister-Event OffHookHandler
			Write-VerboseEvent "Activity changes will be monitored when the client signs in."
			}
		}
	}

function LyncProcess-Handler
	{
	Write-Host "$((Get-Date).ToLongTimeString()) - Lync client has shut down." -ForegroundColor Yellow
	#Client object is invalid if Lync process stops
	Stop-Monitoring
	
	#Restart connection and registration steps
	Connect-LyncClient
	Initialize-Registration
	}

function Initialize-Registration
	{
	#Register for contact changes if client is already signed in
	if ($client.State -eq [Microsoft.Lync.Model.ClientState]::SignedIn)
		{
		Register-ContactChange
		Write-Host "$((Get-Date).ToLongTimeString()) - Activity changes now being monitored." -ForegroundColor Green
		Register-ClientStateChange
		}
	#Register for client changes, which will handle contact change registration
	else
		{
		Register-ClientStateChange
		Write-VerboseEvent "Activity changes will be monitored when the client has signed in."
		}
	}

function Stop-Monitoring
	{
	Unregister-Event OffHookHandler -ErrorAction SilentlyContinue
	Unregister-Event ClientStateHandler -ErrorAction SilentlyContinue
	Unregister-Event LyncProcessHandler -ErrorAction SilentlyContinue
	Write-VerboseEvent "Events unregistered"
	$global:client = $null
	$global:lyncProcess = $null
	}

#EndRegion

#Region Script body

#Check for dot sourcing
if ($MyInvocation.InvocationName -ne '.')
	{
	Write-Error "Script was not dot-sourced.  This script is designed to be executed by dot sourcing it: . " -Category InvalidOperation
	break
	}

#Check for SDK installation
$apiPath = "C:\Windows\assembly\GAC_MSIL\Microsoft.Lync.Model\4.0.0.0__31bf3856ad364e35\Microsoft.Lync.Model.dll"
if (Test-Path $apiPath)
	{
	Add-Type -Path $apiPath
	#Connect to local Lync client
	Connect-LyncClient
	}
else
	{
	Write-Error "This script requires the Lync SDK runtime library." -Category NotInstalled
	break
	}

#Check for Verbose parameter for event functions
if ($MyInvocation.BoundParameters['verbose'])
	{
	$global:verboseEvent = $true
	}
else
	{
	$global:verboseEvent = $false
	}
	
#Start event registration
Initialize-Registration

#EndRegion

Update to script that runs Windows Update

Articles in the "Programmatically Run Windows Update" series

  1. Programmatically run Windows Update (as part of a broader patch and reboot process)
  2. Update to script that runs Windows Update [This article]

As part of the patch and reboot script I use in my Exchange environment, there is a separate script that runs Windows Update, first covered here.  I have updated the script to allow skipping the installation of any patches by specifying the KB numbers in a companion file.

Starting on the second line of the file (so that the first line can contain instructions), list one KB number per line.  To use this list, there is a new function in the script:

function Get-UpdatesToSkip
	{
	$scriptDirectory = Split-Path $script:MyInvocation.MyCommand.Path
	$KB = @()
	if (Test-Path "$scriptDirectory\updatestoskip.txt")
		{
		$sourceFile = Get-Content "$scriptDirectory\updatestoskip.txt"
		if ($sourceFile.Length -gt 1)
			{
			for ($i = 1;$i -le $sourceFile.Length - 1;$i++)
				{
				$KB += $sourceFile[$i]
				}
			}
		}
	$KB
	}

The function gets the current path of the script, checks for the existence of the file, and reads each KB into an array. If the file is not present, or is present but no KB numbers are listed, the array will just be empty. This allows the script to still run successfully when you don’t have the file or specify any patches to skip.

When searching for available updates via the Microsoft.Update.Session interface, you can’t filter on a KB number, but you can filter on an Update ID (UID). Unfortunately, to cross reference KB number and UID, you have to first perform a search anyway. So rather than searching twice, I just work with the collection created from the search. The object that contains the list of available hotfixes cannot be manipulated the same way as a standard collection object since it is a COM object, so you can’t use a method like Remove() to remove an item from the collection.

Therefore, in order to have a collection of updates that don’t include certain KBs, you can build a new collection, skipping items in the first collection that match a filter. To do this, I create another Microsoft.Update.UpdateColl object that will be used to specify which updates to download (so that you don’t download all of the updates and then skip certain ones):

$updatesToDownload = New-Object -ComObject 'Microsoft.Update.UpdateColl'

To add to this collection, each item in the first collection is compared to the array that contains the list of KBs:

$updatesToSkip = Get-UpdatesToSkip
$updates | ForEach-Object {
	if ($updatesToSkip -match $_.KBArticleIDs)
		{
		WriteEvent "Skipping download of $($_.KBArticleIDs) since it is listed in the file of updates to skip." 'Information' '1001'
		}
	else
		{
		$updatesToDownload.Add($_) | Out-Null
		}
	}

(The WriteEvent function is defined in the script and is used to write a custom event to the Application log so that you will know that an update was intentionally skipped.) The rest of the script is basically unchanged; just some minor tweaks to accommodate the new code. The download has been updated and includes the companion text file. (If you are wondering why a companion file is used, it is because, as part of the Exchange patch and reboot script, the Windows Update script is remotely called via a scheduled task, and there isn’t a way to dynamically specify command line parameters for a scheduled task (without modifying the task itself)).

  Run-WindowsUpdate.zip (1.8 KiB)