Delegate management module updated to v1.5.0

It had been some time since I updated the delegate management PowerShell module. I started to update it last year after I decided to move the admin configuration settings to their own file, but didn’t complete it.  After a user of the module recently contacted me about adding one of the supported options for delegate meeting forwarding, I went back and finished updating it.  These are the changes:

  • Configuration settings have been moved to a file.  This allows for drop-in updates to the module without you having to reconfigure your settings again.  The file, DelegateManagementSettings.xml, should be located in the same directory as the module.  A default file is included in the download, but you can also use the new Set-DelegateManagementSetting cmdlet with the CreateDefaultFile parameter to have the module create one for you.  You can run Get-DelegateManagementSetting to view your current settings.  If you want to change one or more settings for the current session, the Set-DelegateManagementSetting cmdlet can do that, and you can use the Persist parameter if you want the changed setting to be written to the file.  (Comment-based help is available for both new cmdlets, e.g., Get-Help Set-DelegateManagementSetting.)
  • The following functionality has been moved from static values to configurable settings: whether to use Autodiscover, specifying an EWS URL, whether to use impersonation, and if permission should be added/set by default for the Deleted Items and Sent Items folders when adding a delegate.
  • The Azure AD module is no longer needed for managing delegates in Exchange Online.
  • The ability to specify that meeting requests for the owner/manager should not be forwarded to delegates.  (This is supported with Exchange 2013+ and Exchange Online mailboxes.)  NoForward has been added as a valid value with the MeetingRequestDeliveryScope parameter.
  • All functions for connecting via PowerShell to on-premises Exchange and Exchange Online have been removed.  Connection to the appropriate environment should be handled externally to the module, as would usually be done with any other module or cmdlet.  The exception to this is for Exchange Web Services (seeing as it is a stateless connection).  If managing delegates in Exchange Online, the first time you run a delegate cmdlet it will prompt for EWS credentials, which will be used for all future cmdlets in the same shell.  On-premises will use the current user credentials.  If you would like the module to be able to store and recall credentials automatically (in Credential Manager, for example), specify credentials for on-premises, or other credential options, let me know.

The updated module, along with a settings file, is here:

  DelegateManagement.zip (8.9 KiB)

Meeting cancellation script updated

Articles in the "Meeting cancellation script" series

  1. Cancel future meetings in a mailbox
  2. Meeting cancellation script updated [This article]

I have updated the Cancel-MailboxMeetings.ps1 script. These are the changes:

  • Changed the date parameters to align with Microsoft’s Remove-CalendarEvents cmdlet.  StartDate is now QueryStartDate and EndDate is now QueryWindowInDays.  The default time frame is still one year from the query start date, but is now specified as an integer of days instead of a specific date.
  • Added a preview mode via the PreviewOnly switch parameter (also to align with Microsoft).  Using the parameter will list which meetings (by subject and start date, first occurrence start date if a recurring meeting) would be affected.  To provide enough detail, it will indicate if it is a standalone meeting or series and whether the meeting is being canceled (because the mailbox is the organizer) or declined (because the mailbox is an attendee).  If you use the EndOrganizedRecurringMeetings parameter, it will also use wording to show that a meeting series’ end date would be updated instead of canceled.
  • Added detection of modified occurrences that will be lost if a recurring series’ end date is changed.  Because exceptions to a meeting (only modifications, not deletions) are removed when a series’ end date is changed, the script checks for modified occurrences that occurred in the past of the query start date and prompts you to confirm that you want to update that meeting.
  • Added a switch parameter named SuppressLostExceptionPrompt.  Use this if you want to prevent the confirmation prompt about lost modified occurrences and have it change the end date anyway.  This parameter only has an effect if also used with EndOrganizedRecurringMeetings.
  • Although added only while I was testing, but decided to leave in, are some extra details written to the screen if you use the Verbose parameter.

The code in the first post has been updated, as has the downloadable version:

  Cancel-MailboxMeetings.ps1 (11.1 KiB)

Cancel future meetings in a mailbox

Articles in the "Meeting cancellation script" series

  1. Cancel future meetings in a mailbox [This article]
  2. Meeting cancellation script updated

One of the long-running woes of the Exchange admin is that you can’t transfer “ownership” of a meeting to someone else when the owner leaves the company. While you still can’t do that, Microsoft recently announced the Remove-CalendarEvents cmdlet for mailboxes in Exchange Online.  Its intention is to allow the administrator to cancel all future meetings for a terminated employee so that someone else can create new meetings in their place.

Because it works only for cloud mailboxes, I wrote a script to do the same thing for on-premises mailboxes (though it will also work for cloud mailboxes).  It uses EWS to get all future meetings for a mailbox and cancel them.  The default settings are to cancel all meetings that occur in the next year where the mailbox is the organizer.  You can also choose to have it cancel (decline) meetings where the mailbox is an attendee, and you can specify text that should be added to the cancellation or decline message.  Additionally, if you don’t want to wholly cancel recurring meetings because the history of all those occurrences will be lost for the attendees, you can instead have organized recurring meetings end so that only future occurrences are canceled, but historical occurrences remain.

Because of the way calendar items are stored, you can get an item and then see when it occurs, if it is recurring, etc., but if it is a recurring meeting you only see the first occurrence.  To work with occurrences, you use a calendar view (time frame) and let Exchange worry about whether an occurrence of a recurring item should be included in the window.  But you can have multiple occurrences of the same meeting in the time frame, and you want to delete the series, not an occurrence.  In a calendar view, you can see if a meeting is an occurrence and, if so, get the corresponding master for that series.

Once the series is canceled (or ended, if you choose that option), there still can be more occurrences of that canceled series in the search results.  If you try and get the recurring master for an occurrence whose master has already been canceled, you’ll get an error.  So, these occurrences can be skipped, but you have to know which ones can be skipped.  To solve that, I store the recurring meeting’s global object ID (which is the same for a recurring master and all of its occurrences because it is really just one meeting as stored in the calendar) in an array.  For every meeting in the search results that is an occurrence, I check if the global object ID is in the array.  If so, I skip it because its master has already been deleted.  If not, I add the global object ID to the array, then cancel the meeting.

The script has comment-based help so you know what all the parameters are for, and there are inline comments so you can see what is being done throughout.  While you can use current credentials or specify one, if you want it to prompt if you don’t specify credentials and don’t want to use default credentials, you can comment and uncomment the the necessary lines at 81-84.  Likewise for the EWS URL, it is hard-coded to EXO, which you can change, but if you want to use autodiscover, you can comment and uncomment the necessary lines at 91-93.

Download the script via the link below, but you can see the code and copy it below, too.

  Cancel-MailboxMeetings.ps1 (11.1 KiB)

<#
	.Synopsis
		Cancel meetings within a specified timeframe for a mailbox
	.Description
		Ideal for terminated employees, this script will cancel all meetings
		organized by the mailbox, and optionally where the mailbox is an attendee,
		within a given timeframe.  The default is one year from run time.
	.Parameter EmailAddress
		Email address of the mailbox to process.  Pipeline support allows piping
		from Get-Mailbox.
	.Parameter QueryStartDate
		Start date of the timeframe to look for meetings to cancel.  Default is now.
	.Parameter QueryWindowInDays
		Number of days after the start date to look for meetings to cancel.  Default is 365 days.
	.Parameter IncludeAttendeeMeetings
		Switch to also cancel meetings where the mailbox is an attendee
	.Parameter EndOrganizedRecurringMeetings
		Switch to end organized recurring meetings instead of canceling them.
		The end date of the series is set to the cmdlet's StartDate.  This will leave 
		past meeting occurrences on attendee calendars.
	.Parameter SuppressLostExceptionPrompt
		Switch to silently acknowledge that past occurrences of series that have
		been modified will be deleted if EndOrganizedRecurringMeetings is used
	.Parameter PreviewOnly
		Switch to display the meeting subjects and dates that would be canceled
	.Parameter CancellationText
		Text to add to the body of the cancellation message
	.Parameter Credential
		Optional credential object to use instead of asking for or using current credentials
	.Parameter UseImpersonation
		Switch parameter to use impersonation instead of FMA or folder permission
	.Example
		Cancel-MailboxMeetings.ps1 -EmailAdress johndoe@company.com
	.Example
		Cancel-MailboxMeetings.ps1 janedoe@company.com "6/1/17" "8/1/17" -IncludeAttendeeMeetings -CancellationText "Jane is no longer here."
	.Example
		Get-Mailbox johndoe | Cancel-MailboxMeetings.ps1 -Credential (Get-Credential) -EndOrganizedRecurringMeetings
	.Notes
		Required permission: 
			*Editor role to calendar will work for meetings (attendee or organizer)
			- or -
			*Impersonation right to the target mailbox
			- or -
			*Full mailbox access; Send As is not required
		Version: 1.3
		Date: 8/3/17
#>
#Version 3 is only required because of use of the [pscredential] type accelerator for the Credential parameter
#requires -Version 3
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]
Param (
	[Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="Email address of user")]
		[Alias("PrimarySMTPAddress")][string]$EmailAddress,
	[Parameter(Position=1,Mandatory=$false)][DateTime]$QueryStartDate = (Get-Date),
	[Parameter(Position=2,Mandatory=$false)][int]$QueryWindowInDays = 365,
	[switch]$IncludeAttendeeMeetings,
	[switch]$EndOrganizedRecurringMeetings,
	[switch]$SuppressLostExceptionPrompt,
	[switch]$PreviewOnly,
	[Parameter(Mandatory=$false)][string]$CancellationText,	
	[Parameter(Mandatory=$false)][PSCredential]$Credential,
	[switch]$UseImpersonation
	)

begin
	{
	#Check for EWS API
	$apiPath = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services" |
				Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name))."Install Directory") + "Microsoft.Exchange.WebServices.dll")
	if (Test-Path $apiPath)
		{
		Add-Type -Path $apiPath
		}
	else
		{
		Write-Error "The Exchange Web Services Managed API is required to use this script." -Category NotInstalled
		break
		}

	$exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
	$exchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion) 
	}

process
	{
	if (-not($Credential))
		{
		#$Credential = Get-Credential
		#$exchangeService.Credentials = New-Object -TypeName Microsoft.Exchange.WebServices.Data.WebCredentials($Credential)
		# Or
		$exchangeService.UseDefaultCredentials = $true
		}
	else
		{
		$exchangeService.Credentials = New-Object -TypeName Microsoft.Exchange.WebServices.Data.WebCredentials($Credential)
		}

	#$exchangeService.AutodiscoverUrl($EmailAddress, {$true})
	#Or use hard-coded URL
	$exchangeService.Url = "https://outlook.office365.com/EWS/Exchange.asmx"

	if ($UseImpersonation)
		{
		$exchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId(
			[Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailbox.EmailAddress)
		}
	
	#Bind to the calendar
	$folderID = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId(
		[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$EmailAddress)
	$calFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService,$folderID)

	#Create a view to get all events during the specified timeframe
	$StartDate = $QueryStartDate
	$EndDate = $StartDate.AddDays($QueryWindowInDays)
	$calendarView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.CalendarView($StartDate, $EndDate)
	$properties = New-Object -TypeName Microsoft.Exchange.WebServices.Data.PropertySet(
		[Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
	#Add Clean Global Object ID to property set so it can be used to correlate occurrences of the same meeting
	$propCleanGOID = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(
		[Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Meeting, 35,
		[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)
	$properties.Add($propCleanGOID)
	$calendarView.PropertySet = $properties
	$searchResult = $exchangeService.FindAppointments($calFolder.Id,$calendarView)

	#region Functions
	function Process-Cancellation ($meeting, $attendeeType, $goid)
		{
		if ($meeting.IsRecurring)
			{
			#Skip processing if recurring master has already been canceled
			if ($meetingIds -notcontains $goid)
				{
				#Determine whether occurrence or master
				if ($meeting.AppointmentType -ne [Microsoft.Exchange.WebServices.Data.AppointmentType]::RecurringMaster)
					{
					#Get recurring master
					$master = [Microsoft.Exchange.Webservices.Data.Appointment]::BindToRecurringMaster($exchangeService,$meeting.Id)
					}
				else
					{
					$master = $meeting
					}
				#Add Global ID to array so remaining occurrences in collection can be ignored
				$meetingIds.Add($goid)
				if ($attendeeType -eq "Organizer")
					{
					#If ending instead of canceling, end only if first occurrence is in the past, else cancel
					if ($EndOrganizedRecurringMeetings -and ($StartDate -gt $master.Start))
						{
						Write-Verbose "EndOrganizedRecurringMeetings is true and first occurrence is in past"
						#Check for modified exceptions that will be lost
						if ($master.ModifiedOccurrences)
							{
							Write-Verbose "Meeting series has modified occurrences"
							$hasException = $false
							foreach ($occurrenceId in $master.ModifiedOccurrences)
								{
								$propertySet = New-Object -TypeName Microsoft.Exchange.WebServices.Data.PropertySet(
									[Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly,
									@([Microsoft.Exchange.WebServices.Data.AppointmentSchema]::Start))
								$occurrence = [Microsoft.Exchange.WebServices.Data.Item]::Bind($exchangeService,$occurrenceId.ItemId,$propertySet)
								if ($occurrence.Start -lt $StartDate)
									{
									Write-Verbose "Meeting series has modified occurrence in the past"
									$hasException = $true
									break
									}
								}
							}
						if ($hasException)
							{
							if ($PreviewOnly)
								{
								Update-MeetingEndDate -meeting $master
								}
							elseif ($PSCmdlet.ShouldProcess("Meeting series with the subject `"$($master.Subject)`" that has one or more past occurrence exceptions that will be lost if the series`' end date is changed") -or $SuppressLostExceptionPrompt)
								{
								Update-MeetingEndDate -meeting $master
								}
							}
						else
							{
							Update-MeetingEndDate -meeting $master
							}
						}
					else
						{
						Send-MeetingCancel -meeting $master 'Series'
						}
					}
				else
					{
					Send-MeetingDecline -meeting $master 'Series'
					}
				}
			}
		else #Non-recurring meeting
			{
			if ($attendeeType -eq "Organizer")
				{
				Send-MeetingCancel -meeting $meeting 'Meeting'
				}
			else
				{
				Send-MeetingDecline -meeting $meeting 'Meeting'
				}
			}
		}

	function Update-MeetingEndDate ($meeting)
		{
		if ($PreviewOnly)
			{
			Write-Output "Series whose end date would be updated: `"$($meeting.Subject)`" on $($meeting.Start.ToShortDateString())"
			}
		else
			{
			$meeting.Recurrence.EndDate = $StartDate
			$meeting.Update([Microsoft.Exchange.WebServices.Data.SendInvitationsMode]::SendToAllAndSaveCopy)
			Write-Output "Series end date updated: $($meeting.Subject)"
			}
		}
	
	function Send-MeetingCancel ($meeting, $type)
		{
		if ($PreviewOnly)
			{
			Write-Output "$type that would be canceled: `"$($meeting.Subject)`" on $($meeting.Start.ToShortDateString())"
			}
		else
			{
			[void]$meeting.CancelMeeting($CancellationText)
			Write-Output "$type canceled: $($meeting.Subject)"
			}
		}
		
	function Send-MeetingDecline ($meeting, $type)
		{
		if ($PreviewOnly)
			{
			Write-Output "$type that would be declined: `"$($meeting.Subject)`" on $($meeting.Start.ToShortDateString())"
			}
		else
			{
			#Decline method does not support custom message, so create decline message
			if ($CancellationText)
				{
				$declineMessage = $meeting.CreateDeclineMessage()
				$declineMessage.Body = $CancellationText
				[void]$declineMessage.Send()
				Write-Output "$type declined: $($meeting.Subject)"
				}
			else
				{
				[void]$meeting.Decline($true)
				Write-Output "$type declined: $($meeting.Subject)"
				}
			}
		}
	#endregion
	
	if ($searchResult.TotalCount -gt 0)
		{
		#Exclude non-meetings
		$meetings = $searchResult.Items | Where-Object {$_.IsMeeting}
		if ($meetings)
			{
			Write-Output "Processing $($meetings.Count) meeting occurrences."
			#Create array to hold list of recurring master IDs
			$meetingIds = New-Object -TypeName System.Collections.Generic.List[System.String]
			foreach ($item in $meetings)
				{
				#Get meeting global ID from extended properties collection
				$cleanGOID = $null
				[void]$item.TryGetProperty($propCleanGOID, [ref]$cleanGOID)
				
				if ($item.MyResponseType -eq [Microsoft.Exchange.WebServices.Data.MeetingResponseType]::Organizer) #Mailbox is organizer
					{
					Process-Cancellation -meeting $item -attendeeType "Organizer" -goid $cleanGOID
					}
				elseif ($IncludeAttendeeMeetings) #Decline meetings where mailbox is attendee
					{
					Process-Cancellation -meeting $item -attendeeType "Attendee" -goid $cleanGOID
					}
				}
			}
		else
			{
			Write-Output "There are no meetings in the specified timeframe."
			}
		}
	else
		{
		Write-Output "There are no meetings in the specified timeframe."
		}
	}

AutoDL module updated yet again

Articles in the "AutoDL management module" series

  1. PowerShell module for managing automated distribution groups
  2. AutoDL module updated yet again [This article]

Edit 3/27/17:  Rather than add another post, this entry has been updated to reflect the addition of the Find Groups button added for group mirroring.

When automating distribution lists, eventually someone will want one that doesn’t use its own filter, but contains the same members as another group.  (Usually it is a security group when there is separation between DLs and security groups.)  The module has always supported group mirroring, but it required a specific syntax (the LDAP filter had to be prefixed with “guid:”) and that the object’s GUID be entered (without curly braces).

The module has been updated so that it is much easier to use group mirroring.  The GUI now has a separate field for entering the groups to mirror:

AutoDL Group Mirroring

A group’s filter now separates using either an LDAP filter or group mirroring.

You select the radio button for either using an LDAP filter or mirroring the membership of other groups.  You no longer manually enter the GUID of the object, but enter the distinguished name of the object and it will be converted to the objectGUID when saving the filter.  (Using the objectGUID allows membership updates to continue even if the source group is moved and its distinguished name changes.) You can also add groups to the list by clicking the Find Groups button and searching for them.  This functionality is provided by an external library that is included in the download.  Put the DLL in the same directory as the module and it will automatically be loaded.  I have added a check for the library, so if it isn’t found, the Find Groups button will simply be disabled and a label will be displayed next to it that says the dependent file could not be loaded.

When running Get-AutoDLFilter and Set-AutoDLFilter, the objectGUIDs will be converted into the current DNs for display.  The GUI output of Get-AutoDLFilter has also been updated to reflect whether it is using an LDAP filter or group mirroring and, if the latter, doesn’t display the sections for the display-formatted and raw filters.

The distinguished name of the object(s) will be validated when the focus leaves the text box (unless clicking the Cancel button), checking that it resolves to an existing group object.  If updating a group’s filter from the command line, the MirrorGroup parameter has been added to the Set-AutoDLFilter cmdlet.  The same DN validation occurs when updating from the command line, and the LDAPFilter and MirrorGroup parameters are in separate parameter sets so that they are mutually exclusive.

Any existing groups using group mirroring are compatible with this version:  Nothing has changed on the “back end,” only how the groups being mirrored get added to a filter has been updated.

Download the module and AD object picker library here:

  AutoDLManagement.zip (22.7 KiB)


If you want just the module, it is here:

  AutoDLManagement.psm1 (55.4 KiB)

AutoDL management module updated

I have spent a fair amount of time updating the AutoDL module I first posted back in December.  At the end of that post I mentioned several things I was considering adding, and that made me want to add some of them.

  • You can now specify users that should always be excluded even though they match the filter:
    Set Filter with Exclude Option

    The ability to exclude users from membership is now an option.

    The Set-AutoDLFilter cmdlet has also been updated with the AlwaysExclude parameter.

  • LDAP syntax checking has been added and occurs when the focus leaves the filter’s text box:
    Example of bad syntax prompt

    LDAP syntax will be validated and present a warning if it does not pass.

    The OK and Preview buttons will be disabled if the syntax check fails.  I looked around for ways to simply and efficiently validate syntax, but I could not find anything besides what is provided via the LDAP interface to Active Directory.  When performing an LDAP search, a specific error is returned if the syntax is not valid, so the module passes the entered filter to AD and checks the response.  (For efficiency with filters that pass, I use the FindOne() method so the search will stop as soon as the first matching object is found.)

  • A preview window has been added:
    AutoDL MemberShip Preview

    Clicking the Preview button opens a new window to show who will be in the DL.

    The preview window will display a sorted list of member display names.  You can copy this list to the clipboard to be able to, for example, paste pending membership into an email of the person requesting the DL for confirmation.  There is also a field that provides the membership count.  If the filter results in no members, the count field will contain a hyphen and the Copy button will be disabled.

  • A parameter, VerboseMembershipChanges, has been added to Update-AutoDL if you want to include in the output (screen and log file) the individual members that were added and removed:

    Detailed Membership Changes

    You can get detailed membership changes in the screen output and log file.

  • The Verbose parameter can be used with Set-AutoDLFilter and Update-AutoDL to get more details of activity.  I added a bunch of Write-Verbose commands while I was debugging these recent changes and I left them in for those that want more information about what is happening.
  • The module has been updated to conform with the recommendations of the PSScriptAnalyzer module.
  • The module now requires PowerShell version 3 or higher and will check for that.
  • Several (well, more than several) minor fixes as a result of previous changes made that weren’t discovered until I was testing in a new lab and had to create new DLs and provision them and do other tasks that are less common once you are only maintaining automated DLs.  This is what modules like Pester are for, I suppose.

You can download the updated module with this link:

  AutoDLManagement.psm1 (55.4 KiB)


(I have started exploring the possibility of hosting a Nuget repository or publishing my modules on the PowerShell Gallery so PowerShell v5+ users can simply install them with a command like Install-Module AutoDLManagement -force.)

PowerShell module for managing automated distribution groups

Articles in the "AutoDL management module" series

  1. PowerShell module for managing automated distribution groups [This article]
  2. AutoDL module updated yet again

Years ago I used a third-party application for creating, managing, and updating automated DLs in Exchange.  When I moved to another company that didn’t have that application, nor would spend the money to buy it, I wrote a script as a poor-man’s version of that application.  Since then, it has been updated with more features, and I recently updated it again for a customer who wanted features I had never needed.

You might wonder why not use dynamic distribution groups?  There are a number of limitations when using DDGs that automated DGs (née DLs) overcome:

  • Use the group as a security principal
  • Allow static members (those that should be members even though they don’t match the filter)
  • Use any LDAP attribute in the schema in the search filter, not just those exposed via OPATH
  • View the group membership (when an end user)

The way the module works is it marks an existing DG as automated and updates the Note property stating that it is an automated group; you enter an LDAP filter to use for membership, optionally specifying any static members and domains to restrict membership to, and save it; and the membership criteria are saved as a binary value in the extensionData attribute.  Then you can run Update-AutoDL to update the membership of that one group or all (or all in a specified domain), or schedule a task to run that cmdlet to update membership, say, daily.  If you want to download it without reading all of the details:

  AutoDLManagement.psm1 (55.4 KiB)

Prerequisites:

  • Permission to manage the group objects in Active Directory
    The module uses ADSI to manage the groups, search for members, and to update membership.
  • A shell with Exchange cmdlets loaded
    A few of the Exchange cmdlets are used to validate that a group exists, and to enable it for automation.

Installation:

  • Put the module in an appropriate directory
    If you want the module to be implicitly loaded, or explicitly loaded without specifying a path, put the module inside a directory of the same name inside the Modules directory of your profile, e.g., C:\Users\<username>\Documents\Windows PowerShell\Modules\AutoDLManagement\AutoDLManagement.psm1.  You can put it anywhere if you want to load it manually and specify the full path to it.
  • For interactive or scheduled execution, load the Exchange cmdlets
    Whether using implicit remoting or a shell loading the Exchange snap-in locally, you’ll want to have the Exchange cmdlets loaded in the session before running any of the AutoDL cmdlets.

Features of the AutoDLManagement module:

  • Uses an LDAP filter as membership criterion
    Note: There is no syntax checking for the filter, so be sure you enter a valid one.
  • Membership criteria are stored in the group itself
    There is no external dependency except for the module to update the group membership.
  • Group can contain static members
    Include recipients that do not match the search filter.  The attribute used to match static members can be customized.  (The default is sAMAccountName.)
  • Restrict membership to specified domains
    The default configuration is to include all matching recipients in the forest, but you can specify one or more domains in the forest that membership is restricted to.
  • Global filter that applies to all groups
    This is a filter that is applied in addition to a group’s filter, such as not including terminated users.
  • Customize which custom attribute (extensionAttributeN) and what text string are used to indicate a group is automated
  • Email notification when a group update fails or membership is zero when it wasn’t previously
    If the module is unable to add members after clearing membership, resulting in no members, you are notified so you don’t end up with a group that people are emailing and no one gets the message.
  • Supports all versions of Exchange, including Exchange Online
    Note: Exchange Online support requires groups to be authoritative on-premises, syncing to Exchange Online via AAD Connect.
  • Display membership criteria in both “raw” and “pretty” formats
    When you run Get-AutoDLFilter, the result is an IE window that displays the LDAP filter, the static members, and domains for membership.  The filter is displayed as both a raw string that you can copy and paste for editing, and as a formatted filter (based on my LDAP-formatting script described here) that can be easier to read and is useful when sending a filter to a user.

  • Mirror membership from other groups
    Sometimes a group owner wants the membership to mirror a security group because it isn’t mail-enabled.  You can mirror membership of one or more groups by setting the LDAP filter to be “guid:<GUID of group>”.  Separate multiple GUIDs with a semicolon.
  • Logging of each group that is updated, including old and new count
  • Ability to update the membership of one group, all groups, or groups in a specified domain
    When running Update-AutoDL, you can provide the identity of a group, use the UpdateDomain parameter to specify a single domain’s groups to update, or no arguments to update all groups in the forest.  (If you update all groups interactively, it will ask for confirmation.  If updated via a scheduled task, the confirmation is suppressed.)
  • Set/Update a group’s criteria via command line or GUI
    Run Set-AutoDLFilter (alias sadl) with the identity of a group.  If the group is not already managed/automated, it will ask if you want to enable it:
    After entering ‘y’, the GUI will display so you can enter membership criteria:
    The form performs basic checks in order to enable the OK button: if static members is checked, then the text box has to be populated; if domain restrictions is checked, then at least one domain must be checked.When you run the cmdlet with a group that already has membership criteria, the form will be populated with the appropriate values, so you only have to modify what is to be changed about the criteria.For command-line updating, use the appropriate parameters for the filter, static members, and domain restrictions.  (The parameters are documented in the cmdlet’s help.)  If you update from the command-line, you need to include all values even if it isn’t changing.  For example, if you are modifying the LDAP filter for a group that has static members, you need to specify those members even if they aren’t changing.  Otherwise, the static members (or domain restrictions) will be lost.
  • Pipeline support
    For bulk-enabling and -changing groups, you can pipe groups to the Set-AutoDLFilter cmdlet.  This allows you to bulk-enable groups for automation and set their membership criteria if you already have that information in, say, a CSV.  You can use a switch parameter with the cmdlet to suppress the confirmation prompt to enable automation for a group.

Functionality that I am considering adding in the future:

  • Static exclusions (Now Added)
    If there are recipients you don’t want to be in the group even though they match the criteria, you can specify them.  (You can do this now by adding the username to the filter with a NOT operator, e.g., (!samaccountname=johndoe).)
  • Add object picker for selecting groups for mirrored membership
    Currently, you need to manually enter the source group’s GUID in the LDAP filter field (preceded by guid:).  The object picker would let you browse the forest and select a group to have its GUID automatically entered.
  • Perform syntax checking of an LDAP filter (Now Added)
  • Preview the pending membership of a group (Now Added)
    In the criteria editor, you could click a button to preview the membership based on the criteria currently specified (and without having to save it).
  • Add/remove only changed members (Module already does this)
    Currently, group membership is cleared and then updated with all objects returned in the search.  This has an element of risk because if a failure occurs between clearing and adding membership, the group is left with no members.  This feature would compare the current and pending memberships and only add/remove the necessary objects.

If these or other features would be of interest to you, let me know in the comments.  Download the module:

  AutoDLManagement.psm1 (55.4 KiB)