Export and Import Calendar Processing Information
During my current project, it became necessary to capture additional calendar processing parameters that are not preserved during a normal hybrid move--such as booking policies.
Some of the challenges that I faced with this tool:
- Blank or unpopulated attributes
- Conversion of sAMAccountName values to PrimarySmtpAddress
- Multiline attributes with special characters
- Attributes that were set for the wrong recipient type
So, the first thoughts I have when building a tool generally involve four concepts: what data do I need to gather/export, what format is the source data, what format do I need to save it in, and how do restore/import that data back into the target?
Gathering Data
In this case, I knew I needed to gather calendar booking and processing information, so I immediately knew I would be using the Get-CalendarProcessing cmdlet. Running it against a sample of resource mailboxes (shared, room, equipment) revealed the types of data that can be stored in each attribute. The help on Get-CalendarProcessing is pretty sparse, so I spent most of my time looking a Get-CalendarProcesssing -identity user@domain.com | fl and help for Set-CalendarProcessing to learn what properly formatted data inputs would look like. When I sit down to build a tool, I try to make it as generic as possible and try to make it work for every environment that someone would want to use it in, which frequently means working with properties and attributes that I might not normally manipulate. In addition, not all objects will have all attributes populated, so I needed to handle that possibility as well (on the import side).
Source Data Format
The result of Get-CalendarProcessing shows that there are a lot of data types to deal with--Boolean attributes (true/false), integers, arrays of characters, and a text area. The text area was the hardest for me to deal with (though the solution was quite simple).
Saving the Export
I determined that a standard CSV would probably be the best format to store this data (it's human readable, supports columnar data, and very easy to work with). To create it, I would simply run Get-CalendarProcessing -Identity user@domain.com | Select Attribute1,Attribute2,etc | Export-Csv filename.csv -NoTypeInformation. Boolean fields are pretty simple, as are fields that only take a single string. An attribute such as ResourceDelegates, however, is an array that can have multiple members. I decided to handle them in this fashion:
@{n='ResourceDelegates';e={$val1=@();foreach ($obj in $_.ResourceDelegates){ $val1 += (Get-Recipient $obj).PrimarySmtpAddress} $val1 -join ";"}}
What this construct does is create a column named ResourceDelegates in the CSV, passes the values stored in the current pipeline object's .ResourceDelegates attribute through a loop and joins them with a semicolon. The result is a value that might look like delegate1@domain.com;delegate2@domain.com;delegate3@domain.com.
Importing the Data
When importing this data, I'm going to want to check to make sure the attribute has data before I import it, and if it does, save it. Invoke-Expression is the perfect way to do this:
$cmd = "Set-CalendarProcessing -Identity $($Mailbox.PrimarySmtpAddress)"
And then, I can concatenate strings, allowing me to build an command or expression to execute later. A simple "If exists" check should tell me if the attribute has data, and then if it does, add it to the string that we will execute via Invoke-Expression later:
If ($Mailbox.AutomateProcesing) { $AutomateProcessing = $Mailbox.AutomateProcessing; $cmd = $cmd + " -AutomateProcessing $AutomateProcessing" }
Sweet. Now I can build a script.
Putting It All Together
After the requisite boilerplate things I put in all of my Exchange and Office 365 scripts (parameters for import/export, import/export URIs, filename, and credentials), I start really building.
The first step, like I said earlier, is to gather all of the mailbox data. One of the projects I'm working on is an Office 365 Dedicated to Office 365 multitenant project, where the Office 365 Dedicated environment has mailboxes belonging to dozens of departments and agencies, each with their own set of domains, divesting to individual Office 365 multitenant environments. So, I need to filter the mailboxes I'm going to export in two ways:
- by domain
- by recipient type
The first task is achieved by passing a -Domain parameter with a domain value, which then evaluates it. The Get-Mailbox -Filter parameter is finicky and doesn't let you use the -match operator, so you need to use -like (which only works with wildcard matching). Without a valid wildcard, -like is essentialy -eq.
# Create the domain filter
If ($Domain)
{
If ($Domain.StartsWith("*"))
{
# Value already starts with an asterisk
}
Else
{
$Domain = "*" + $Domain
}
$Filter = [scriptblock]::Create("`"$Domain`"")
Write-Host -NoNewline "Domain Filter is ";Write-Host -ForegroundColor Green $Filter
}
Also, only shared, room, and equipment mailboxes have any of the parameters we're looking for, so we're going to want to restrict the output to those:
$cmd = "Get-Mailbox -ResultSize Unlimited -Filter { WindowsEmailAddress -like $Filter } -RecipientTypeDetails RoomMailbox,SharedMailbox,EquipmentMailbox"
[array]$Resources = Invoke-Expression $cmd
One of the drawbacks of the Get-CalendarProcessing cmdlet is that it doesn't have the primary SMTP address of the mailbox that you're working on. Its identity is in formatted like a CN, making it a poor choice of identifier if you're moving between Exchange organizations. So, we're going to need to attach a property to each object in the Resources array so we have a way to reference it later:
$ResourceMailboxesSettings = @()
Foreach ($obj in $Resources)
{
$ResourceMailboxSetting = Get-CalendarProcessing $obj.PrimarySmtpAddress
Add-Member -InputObject $ResourceMailboxSetting -MemberType NoteProperty -Name "PrimarySmtpAddress" -Value $obj.PrimarySmtpAddress
$ResourceMailboxesSettings += $ResourceMailboxSetting
}
$ResourceMailboxesSettings | Select PrimarySmtpAddress,`
AutomateProcessing,`
AllowConflicts,`
BookingWindowInDays,`
MaximumDurationInMinutes,`
AllowRecurringMeetings,`
EnforceSchedulingHorizon,`
ScheduleOnlyDuringWorkHours,`
ConflictPercentageAllowed,`
MaximumConflictInstances,`
ForwardRequestsToDelegates,`
DeleteAttachments,`
DeleteComments,`
RemovePrivateProperty,`
DeleteSubject,`
AddOrganizerToSubject,`
DeleteNonCalendarItems,`
TentativePendingApproval,`
EnableResponseDetails,`
OrganizerInfo,`
@{n='ResourceDelegates';e={$val1=@();foreach ($obj in $_.ResourceDelegates){ $val1 += (Get-Recipient $obj).PrimarySmtpAddress} $val1 -join ";"}},`
@{n="RequestOutOfPolicy";e={$val2=@();foreach ($obj in $_.RequestOutOfPolicy){ $val2 += (Get-Recipient $obj).PrimarySmtpAddress} $val2 -join ";"}},`
AllRequestOutOfPolicy,`
@{n="BookInPolicy";e={$val3=@();foreach ($obj in $_.BookInPolicy){ $val3 += (Get-Recipient $obj).PrimarySmtpAddress} $val3 -join ";"}},`
AllBookInPolicy,`
@{n="RequestInPolicy";e={$val4=@();foreach ($obj in $_.RequestInPolicy){ $val4 += (Get-Recipient $obj).PrimarySmtpAddress} $val4 -join ";"}},`
AllRequestInPolicy,`
AddAdditionalResponse,`
AdditionalResponse,`
RemoveOldMeetingMessages,`
AddNewRequestsTentatively,`
ProcessExternalMeetingMessages,`
RemoveForwardedMeetingNotifications `
| Export-Csv -NoTypeInformation $ExportFile -Append
Now, we've got a CSV that has columns of data, the objects in ResourceDelegates, RequestOutOfPolicy, BookInPolicy, and RequestInPolicy have been converted to SMTP Addresses.
Putting the Data Back
The import processing is similar--import the CSV, split the fields that have been joined by a semicolon, only include parameters for attributes that had values, and then only process the applicable attributes for the recipient types.
[array]$ResourceMailboxSettings = Import-Csv $ImportFile
Foreach ($Mailbox in $ResourceMailboxSettings)
{
$RecipientType = (Get-Mailbox $Mailbox.PrimarySmtpAddress).RecipientTypeDetails
$cmd = "Set-CalendarProcessing -Identity $($Mailbox.PrimarySmtpAddress)"
If ($Mailbox.AutomateProcesing) { $AutomateProcessing = $Mailbox.AutomateProcessing; $cmd = $cmd + " -AutomateProcessing $AutomateProcessing" }
# objects that had to be joined; replacing the ";" separator with a "," will cause Set-CalendarProcessing to see the input object as an array
If ($Mailbox.RequestInPolicy) { $RequestInPolicy = $Mailbox.RequestInPolicy.Replace(";",","); $cmd = $cmd + " -RequestInPolicy $RequestInPolicy" }
During my current project, there were some mismatches in object types (room mailbox in source environment, shared mailbox in target), resulting in some attributes that would fail import due to not being acceptable on an object type. In order to work around those and get as many attributes imported as possible, I identified those attributes only applicable to resources and noted them in an output log. To filter on attributes only applicable to the target resource types:
If ($Mailbox.ProcessExternalMeetingMessages)
{
If ($RecipientType -match "Equipment|Room")
{
$ProcessExternalMeetingMessages = "`$"+$Mailbox.ProcessExternalMeetingMessages; $cmd = $cmd + " -ProcessExternalMeetingMessages $ProcessExternalMeetingMessages"
}
Else
{
Write-Host -ForegroundColor Red "Object $($Mailbox.PrimarySmtpAddress) has value for ProcessExternalMeetingMessages, but is not configured as a resource mailbox."
$data = "Object $($Mailbox.PrimarySmtpAddress) has value for ProcessExternalMeetingMessages, but is not configured as a resource mailbox."
$data | Out-File $Logfile -Append
}
}
The final challenge was the data in the AdditionalResponse attribute. It's a text area, and can take HTML code and control characters (such as CRLF). Simply adding the attribute using the same syntax I had previously -- " -Attribute $Attribute" -- wasn't working. Watching the expansion of the variable, it was evident that the value needed to be quoted, and in order to achieve this, I had to double-double-quote (if that's a term) the variable inside the $cmd = $cmd + statement:
$AdditionalResponse = $Mailbox.AdditionalResponse; $cmd = $cmd + " -AdditionalResponse ""$AdditionalResponse"" "
With that done, the data is able to be imported.
The full script is available at https://gallery.technet.microsoft.com/Export-and-Import-Calendar-123866af.
Comments
- Anonymous
January 20, 2017
wow great stuff as always.i never even noticed these things dont carry overgood to knowThanks again- Anonymous
February 08, 2017
Yes, I had to learn the hard way as well. They should carry over in normal hybrid moves, as the mailbox permissions and attributes are synced/sourced in local AD. However, in tenant-to-tenant or D-to-MT divestiture scenarios, they are not. ?
- Anonymous
- Anonymous
November 29, 2017
On premise Exch16:For each resource room we get this:Cannot process argument transformation on parameter 'Identity'. Cannot convert the "Roomname.ConfRm@domain.com" value of type "Microsoft.Exchange.Data.SmtpAddress" to type "Microsoft.Exchange.Configuration.Tasks.MailboxIdParameter". + CategoryInfo : InvalidData: (:) [Get-CalendarProcessing], ParameterBindin...mationException + FullyQualifiedErrorId : ParameterArgumentTransformationError,Get-CalendarProcessing + PSComputerName : exch02.domain.comAdd-Member : Cannot bind argument to parameter 'InputObject' because it is null.At C:\ExportImport-CalendarProcessing.ps1:144 char:28+ Add-Member -InputObject $ResourceMailboxSetting -MemberTy ...+ ~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidData: (:) [Add-Member], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.AddMemberCommand- Anonymous
December 01, 2017
I updated the script in the gallery and it should work correctly. When gathering a lists of mailboxes, I had [array]$mailboxes = (Get-Mailbox $obj.PrimarySmtpAddress), but in some versions of Exchange, it appears that PrimarySmtpAddress is also an object containing a prefix and address, so I added the .ToString() method and it seems to have fixed it.
- Anonymous