diff --git a/.build/cspell-words.txt b/.build/cspell-words.txt index 2e5e09aab..fa3091ccc 100644 --- a/.build/cspell-words.txt +++ b/.build/cspell-words.txt @@ -122,6 +122,7 @@ PKCE Pkcs Prelicensing PROCMON +Prosumer QWORD Redistributable regedit diff --git a/Calendar/BookingHelpers/BookingCustomQuestionHelpers.ps1 b/Calendar/BookingHelpers/BookingCustomQuestionHelpers.ps1 new file mode 100644 index 000000000..e915ef45d --- /dev/null +++ b/Calendar/BookingHelpers/BookingCustomQuestionHelpers.ps1 @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function Get-GraphBookingsCustomQuestions { + param( + [Parameter(Mandatory = $true)] + [string]$Identity + ) + $MBcustomQuestions = Get-MgBookingBusinessCustomQuestion -BookingBusinessId $Identity + $CustomQuestions = @() + foreach ($CustomQuestion in $MBcustomQuestions) { + $CustomQuestions += [PSCustomObject]@{ + Id = $CustomQuestion.Id + DisplayName = $CustomQuestion.DisplayName + AnswerInputType = $CustomQuestion.AnswerInputType + Options = $CustomQuestion.AnswerOptions | ConvertTo-Json -Depth 10 + CreatedDateTime = $CustomQuestion.AdditionalProperties["createdDateTime"] + LastUpdatedDateTime = $CustomQuestion.AdditionalProperties["lastUpdatedDateTime"] + } + } + return $CustomQuestions +} diff --git a/Calendar/BookingHelpers/BookingGenericFunctions.ps1 b/Calendar/BookingHelpers/BookingGenericFunctions.ps1 new file mode 100644 index 000000000..5071ff83a --- /dev/null +++ b/Calendar/BookingHelpers/BookingGenericFunctions.ps1 @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function SplitDomainFromEmail { + param([string] $Email) + return [string]$Email.Split("@")[1] +} + +function SplitIdentityFromEmail { + param([string] $Email) + return [string]$Email.Split("@")[0] +} + +function IsConsumerMailbox { + param([string]$Identity) + + try { + $ConsumerMb = Get-ConsumerMailbox $Identity -ErrorAction Ignore + return [boolean]$ConsumerMb.IsProsumerConsumerMailbox -or $ConsumerMb.IsMigratedConsumerMailbox -or $ConsumerMb.IsPremiumConsumerMailbox + } catch { + return $false #consumer mailbox throws error if domain mailbox + } +} + +function CheckEXOConnection { + if (Get-Command -Name Get-Mailbox -ErrorAction SilentlyContinue) { + Write-Host "Validated connection to Exchange Online..." -ForegroundColor Green + } else { + Write-Error "Get-Mailbox cmdlet not found. Please validate that you are running this script from an Exchange Management Shell and try again." + Write-Host "Look at Import-Module ExchangeOnlineManagement and Connect-ExchangeOnline." + exit + } +} + +function WriteTestResult { + param( + [string]$Title, + [System.Boolean]$Success, + [string]$ErrorMessage, + [bool]$WriteMessageAlways = $false + ) + Write-Host ($Title.PadRight($script:PadCharsMessage) + " : ") -NoNewline + if ($Success) { + if ($WriteMessageAlways) { + WriteGreenCheck + Write-Host (" (" + $ErrorMessage + " )") -ForegroundColor Yellow + } else { + WriteGreenCheck -NewLine + } + } else { + WriteRedX + Write-Host (" (" + $ErrorMessage + " )") -ForegroundColor Red + } +} + +function WriteGreenCheck { + param ( + [parameter()] + [switch]$NewLine + ) + $GreenCheck = @{ + Object = [Char]8730 + ForegroundColor = 'Green' + NoNewLine = if ($NewLine.IsPresent) { $false } else { $true } + } + Write-Host @greenCheck +} + +function WriteRedX { + param ( + [parameter()] + [switch]$NewLine + ) + + $RedX = @{ + Object = [Char]10060 + ForegroundColor = 'Red' + NoNewLine = if ($NewLine.IsPresent) { $false } else { $true } + } + Write-Host @redX +} + +function Convert-ArrayToMultilineString { + param ( + [Array]$Array2D + ) + + # Initialize an empty string + $OutputString = "" + + # Loop through each row (key-value pair) of the array + foreach ($Pair in $Array2D) { + # Ensure the array has exactly two elements (key and value) + if ($Pair.Count -eq 2) { + # Append the key and value to the output string in "key: value" format + $OutputString += "$($Pair[0]): $($Pair[1])`n" + } else { + Write-Warning "Array row does not have exactly 2 elements: $Pair" + } + } + + # Return the multi-line string + return $OutputString.TrimEnd("`n") +} diff --git a/Calendar/BookingHelpers/BookingMBHelpers.ps1 b/Calendar/BookingHelpers/BookingMBHelpers.ps1 new file mode 100644 index 000000000..c4db6ccbd --- /dev/null +++ b/Calendar/BookingHelpers/BookingMBHelpers.ps1 @@ -0,0 +1,237 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function GetBookingMBData { + param ( + [string]$Identity + ) + + $script:BMB = Get-Mailbox -Identity $Identity -ErrorAction SilentlyContinue + if ($null -eq $script:BMB) { + Write-DashLineBoxColor -Line "Booking Mailbox not found" -Color "Red" + return $null + } + + return [PSCustomObject]@{ + DisplayName = $script:BMB.DisplayName + Identity = $script:BMB.Identity + RecipientType = $script:BMB.RecipientType #: UserMailbox + RecipientTypeDetails = $script:BMB.RecipientTypeDetails # SchedulingMailbox + EmailAddresses = $script:BMB.EmailAddresses + IsMailboxEnabled = $script:BMB.IsMailboxEnabled + HiddenFromAddressListsEnabled = $script:BMB.HiddenFromAddressListsEnabled + IsSoftDeletedByRemove = $script:BMB.IsSoftDeletedByRemove + IsSoftDeletedByDisable = $script:BMB.IsSoftDeletedByDisable + IsInactiveMailbox = $script:BMB.IsInactiveMailbox + WhenSoftDeleted = $script:BMB.WhenSoftDeleted + WindowsEmailAddress = $script:BMB.WindowsEmailAddress + WhenCreated = $script:BMB.WhenCreated + Guid = $script:BMB.Guid + OriginatingServer = $script:BMB.OriginatingServer + } +} + +function Get-MgBookingBusinessCache { + param( + [string]$BookingBusinessId + ) + + if ($null -eq $Script:cachedGetMgBookingBusiness) { $Script:cachedGetMgBookingBusiness = @{} } + # Keep in mind that key would be case sensitive + if ($Script:cachedGetMgBookingBusiness.ContainsKey($BookingBusinessId)) { + return $Script:cachedGetMgBookingBusiness[$BookingBusinessId] + } + + $GraphBookingBusiness = Get-MgBookingBusiness -BookingBusinessId $BookingBusinessId + + # Determine if/how you want to handle a possible error from the cmdlet or a null value here + $Script:cachedGetMgBookingBusiness.Add($BookingBusinessId, $GraphBookingBusiness) + return $GraphBookingBusiness +} + +function GetGraphBookingBusiness { + param ( + [string]$Identity + ) + + $GraphBookingBusiness = Get-MgBookingBusinessCache -BookingBusinessId $Identity + + return [PSCustomObject]@{ + DisplayName = $GraphBookingBusiness.DisplayName + OwnerEmail = $GraphBookingBusiness.Email + IsPublished = $GraphBookingBusiness.IsPublished + DefaultCurrencyIso = $GraphBookingBusiness.DefaultCurrencyIso + DefaultTimeZone = $GraphBookingBusiness.DefaultTimeZone + LanguageTag = $GraphBookingBusiness.LanguageTag + Phone = $GraphBookingBusiness.Phone + PublicUrl = $GraphBookingBusiness.PublicUrl + WebSiteUrl = $GraphBookingBusiness.WebSiteUrl + CreatedDateTime = $GraphBookingBusiness.AdditionalProperties["createdDateTime"] + lastUpdatedDateTime = $GraphBookingBusiness.AdditionalProperties["lastUpdatedDateTime"] + Address = "City= " + $GraphBookingBusiness.Address.City + ", `r`n" + + "CountryOrRegion= " + $GraphBookingBusiness.Address.CountryOrRegion + ", `r`n" + + "PostalCode= " + $GraphBookingBusiness.Address.PostalCode + ", `r`n" + + "State= " + $GraphBookingBusiness.Address.State + ", `r`n" + + "Street= " + $GraphBookingBusiness.Address.Street + } +} + +function GetGraphBookingBusinessPage { + param ( + [string]$Identity + ) + + $GraphBookingBusiness = Get-MgBookingBusinessCache -BookingBusinessId $Identity + + $BookingBusinessArray =$null + $BookingBusinessArray = @() + foreach ( $PageSetting in $GraphBookingBusiness.AdditionalProperties.bookingPageSettings.Keys) { + $BookingBusinessArray += [PSCustomObject]@{ + Key = $PageSetting + Value = $GraphBookingBusiness.AdditionalProperties.bookingPageSettings[$PageSetting] + } + } + + return $BookingBusinessArray +} + +function GetGraphBookingBusinessBookingPolicy { + param ( + [string]$Identity + ) + + $GraphBookingBusiness = Get-MgBookingBusinessCache -BookingBusinessId $Identity + + $BookingBusinessArray =$null + $BookingBusinessArray = @() + + $BookingBusinessArray += [PSCustomObject]@{ + Key = "AllowStaffSelection" + Value = $GraphBookingBusiness.SchedulingPolicy.AllowStaffSelection + } + $BookingBusinessArray += [PSCustomObject]@{ + Key = "MaximumAdvance" + Value = "$($GraphBookingBusiness.SchedulingPolicy.MaximumAdvance.Days) days, $($GraphBookingBusiness.SchedulingPolicy.MaximumAdvance.Hours) hours and $($GraphBookingBusiness.SchedulingPolicy.MaximumAdvance.Minutes) minutes" + } + $BookingBusinessArray += [PSCustomObject]@{ + Key = "MinimumLeadTime" + Value = "$($GraphBookingBusiness.SchedulingPolicy.MinimumLeadTime.Days) days, $($GraphBookingBusiness.SchedulingPolicy.MinimumLeadTime.Hours) hours and $($GraphBookingBusiness.SchedulingPolicy.MinimumLeadTime.Minutes) minutes" + } + $BookingBusinessArray += [PSCustomObject]@{ + Key = "SendConfirmationsToOwner" + Value = $GraphBookingBusiness.SchedulingPolicy.SendConfirmationsToOwner + } + $BookingBusinessArray += [PSCustomObject]@{ + Key = "TimeSlotInterval" + Value = "$($GraphBookingBusiness.SchedulingPolicy.TimeSlotInterval.Hour) hours and $($GraphBookingBusiness.SchedulingPolicy.TimeSlotInterval.Minute) minutes" + } + $BookingBusinessArray += [PSCustomObject]@{ + Key = "isMeetingInviteToCustomersEnabled" + Value = $GraphBookingBusiness.SchedulingPolicy.AdditionalProperties["isMeetingInviteToCustomersEnabled"] + } + + return $BookingBusinessArray +} + +function GetGraphBookingBusinessWorkingHours { + param ( + [string]$Identity + ) + + $GraphBookingBusiness = Get-MgBookingBusinessCache -BookingBusinessId $Identity + + $BookingBusinessArray =$null + $BookingBusinessArray = @() + $BookingBusinessArray += [PSCustomObject]@{ + Monday = "" + Tuesday = "" + Wednesday = "" + Thursday = "" + Friday = "" + Saturday = "" + Sunday = "" + } + # need to run loop so that I get a 2Dimensional data array at the end with the string values usable by Excel Export Module + $Max = 0 + for ($I = 0; $I -le 7; $I++) { + $Max = [System.Math]::Max($Max, $GraphBookingBusiness.BusinessHours[0].TimeSlots.Count ) + } + + for ($I = 0; $I -le $Max; $I++) { + $Monday = "" + $Tuesday = "" + $Wednesday = "" + $Thursday = "" + $Friday = "" + $Saturday = "" + $Sunday = "" + + if ($GraphBookingBusiness.BusinessHours[0].TimeSlots) { + if ($I -ge $GraphBookingBusiness.BusinessHours[0].TimeSlots.Count) { + $Monday = "" + } else { + $Monday = $GraphBookingBusiness.BusinessHours[0].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[0].TimeSlots[$I].EndTime.Substring(0, 8) + } + } + if ($GraphBookingBusiness.BusinessHours[1].TimeSlots) { + # $Tuesday = $I -ge $GraphBookingBusiness.BusinessHours[1].TimeSlots.Count ? "": $GraphBookingBusiness.BusinessHours[1].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[1].TimeSlots[$I].EndTime.Substring(0, 8) + if ($I -ge $GraphBookingBusiness.BusinessHours[1].TimeSlots.Count) { + $Tuesday = "" + } else { + $Tuesday = $GraphBookingBusiness.BusinessHours[1].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[1].TimeSlots[$I].EndTime.Substring(0, 8) + } + } + if ($GraphBookingBusiness.BusinessHours[2].TimeSlots) { + # $Wednesday = $I -ge $GraphBookingBusiness.BusinessHours[2].TimeSlots.Count ? "": $GraphBookingBusiness.BusinessHours[2].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[2].TimeSlots[$I].EndTime.Substring(0, 8) + if ($I -ge $GraphBookingBusiness.BusinessHours[2].TimeSlots.Count) { + $Wednesday = "" + } else { + $Wednesday = $GraphBookingBusiness.BusinessHours[2].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[2].TimeSlots[$I].EndTime.Substring(0, 8) + } + } + if ($GraphBookingBusiness.BusinessHours[3].TimeSlots) { + # $Thursday = $I -ge $GraphBookingBusiness.BusinessHours[3].TimeSlots.Count ? "": $GraphBookingBusiness.BusinessHours[3].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[3].TimeSlots[$I].EndTime.Substring(0, 8) + if ($I -ge $GraphBookingBusiness.BusinessHours[3].TimeSlots.Count) { + $Thursday = "" + } else { + $Thursday = $GraphBookingBusiness.BusinessHours[3].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[3].TimeSlots[$I].EndTime.Substring(0, 8) + } + } + if ($GraphBookingBusiness.BusinessHours[4].TimeSlots) { + # $Friday = $I -ge $GraphBookingBusiness.BusinessHours[4].TimeSlots.Count ? "": $GraphBookingBusiness.BusinessHours[4].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[4].TimeSlots[$I].EndTime.Substring(0, 8) + if ($I -ge $GraphBookingBusiness.BusinessHours[4].TimeSlots.Count) { + $Friday = "" + } else { + $Friday = $GraphBookingBusiness.BusinessHours[4].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[4].TimeSlots[$I].EndTime.Substring(0, 8) + } + } + if ($GraphBookingBusiness.BusinessHours[5].TimeSlots) { + # $Saturday = $I -ge $GraphBookingBusiness.BusinessHours[5].TimeSlots.Count ? "": $GraphBookingBusiness.BusinessHours[5].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[5].TimeSlots[$I].EndTime.Substring(0, 8) + if ($I -ge $GraphBookingBusiness.BusinessHours[5].TimeSlots.Count) { + $Saturday = "" + } else { + $Saturday = $GraphBookingBusiness.BusinessHours[5].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[5].TimeSlots[$I].EndTime.Substring(0, 8) + } + } + if ($GraphBookingBusiness.BusinessHours[6].TimeSlots) { + # $Sunday = $I -ge $GraphBookingBusiness.BusinessHours[6].TimeSlots.Count ? "": $GraphBookingBusiness.BusinessHours[6].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[6].TimeSlots[$I].EndTime.Substring(0, 8) + if ($I -ge $GraphBookingBusiness.BusinessHours[6].TimeSlots.Count) { + $Sunday = "" + } else { + $Sunday = $GraphBookingBusiness.BusinessHours[6].TimeSlots[$I].StartTime.Substring(0, 8) + " to " + $GraphBookingBusiness.BusinessHours[6].TimeSlots[$I].EndTime.Substring(0, 8) + } + } + + $BookingBusinessArray += [PSCustomObject]@{ + Monday = $Monday + Tuesday = $Tuesday + Wednesday = $Wednesday + Thursday = $Thursday + Friday = $Friday + Saturday = $Saturday + Sunday = $Sunday + } + } + + return $BookingBusinessArray +} diff --git a/Calendar/BookingHelpers/BookingMBLogic.ps1 b/Calendar/BookingHelpers/BookingMBLogic.ps1 new file mode 100644 index 000000000..b541600cd --- /dev/null +++ b/Calendar/BookingHelpers/BookingMBLogic.ps1 @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function RunMBTests { + param( [string]$Identity ) + + [ref]$ErrorMessage = $null + [ref]$WriteMessageAlways = $true + $TestResult = $true + + Write-DashLineBoxColor "Running Mailbox Tests" -Color Blue + + $ErrorMessage = "" + $TestResult = CheckIdentityIsBookingsMB -Identity $Identity -errorMessage $ErrorMessage + WriteTestResult "Mailbox is Scheduling type " -success $TestResult -errorMessage $ErrorMessage + + $TestResult = CheckIfMBIsHiddenInGAL -Identity $Identity -errorMessage $ErrorMessage + WriteTestResult "Is MB Hidden in GAL" -success $TestResult -errorMessage $ErrorMessage + + $TestResult = CheckBookingsMBEmailAddresses -Identity $Identity -errorMessage $ErrorMessage + WriteTestResult "Check MB Email Addresses" -success $TestResult -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways + WriteBookingsMBEmailAddresses +} + +function CheckIdentityIsBookingsMB { + param( + [string]$Identity, + [ref]$ErrorMessage + ) + Write-Verbose "Checking if mailbox $Identity is Bookings mailbox" + if ($script:BookingMBData.RecipientTypeDetails -ne "SchedulingMailbox") { + $ErrorMessage.Value ="Mailbox $Identity is not a Bookings mailbox " + $script:BookingMBData.RecipientTypeDetails + return $false + } + return $true +} +function CheckIfMBIsHiddenInGAL { + param( + [string]$Identity, + [ref]$ErrorMessage + ) + Write-Verbose "Checking if a MB is Hidden in the GAL" + if ($script:BookingMBData.HiddenFromAddressListsEnabled -eq $true) { + $ErrorMessage.Value = "Mailbox $Identity is Hidden in the GAL." + return $false + } + return $true +} + +function CheckBookingsMBEmailAddresses { + param( + [string]$Identity, + [ref]$ErrorMessage + ) + Write-Verbose "Checking if mailbox $Identity has more than 1 alias" + if ($script:BookingMBData.EmailAddresses.Count -gt 1) { + $ErrorMessage.Value = "Mailbox $Identity has more than one email address" + return $true + } + return $true +} + +function WriteBookingsMBEmailAddresses { + param( [string]$Identity) + Write-Verbose "Checking if mailbox $Identity has the correct email addresses" + + $script:BookingMBData.EmailAddresses | ForEach-Object { Write-Output "$Script:indent$_" } +} diff --git a/Calendar/BookingHelpers/BookingMessageTrackingLogHelper.ps1 b/Calendar/BookingHelpers/BookingMessageTrackingLogHelper.ps1 new file mode 100644 index 000000000..c7644ba1a --- /dev/null +++ b/Calendar/BookingHelpers/BookingMessageTrackingLogHelper.ps1 @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function GetMessageTrackingLog { + param($Identity) + # Get the Message Tracking Log + $DateNow = Get-Date + $DateStart = $DateNow.AddDays($script:MessageTrackingDays*-1) + $DateEnd = Get-Date + $MessageTrackingLog = Get-MessageTrace -SenderAddress $Identity -StartDate $DateStart.ToString("MM/dd/yyyy HH:mm") -EndDate $DateEnd.ToString("MM/dd/yyyy HH:mm") #-ErrorAction SilentlyContinue + return $MessageTrackingLog +} diff --git a/Calendar/BookingHelpers/BookingMessageTrackingLogLogic.ps1 b/Calendar/BookingHelpers/BookingMessageTrackingLogLogic.ps1 new file mode 100644 index 000000000..b3dd94110 --- /dev/null +++ b/Calendar/BookingHelpers/BookingMessageTrackingLogLogic.ps1 @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function RunMessageTrackingLogValidation { + [ref]$ErrorMessage = $null + [ref]$WriteMessageAlways = $false + $TestResult = $true + + Write-DashLineBoxColor "Running Message Tracking Log Validation" -Color Blue + + $ErrorMessage = "" + $TestResult = CheckMessageTrackingLogs -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways + WriteTestResult "Collecting Message tracing logs " -success $TestResult -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways +} + +function CheckMessageTrackingLogs { + param([ref]$ErrorMessage) + Write-Verbose "Collect Message tracking logs for Booking MB" + if ($null -eq $script:MessageTrackingLogs) { + $ErrorMessage.Value ="Message Tracking Logs are null " + return $false + } + + $ErrorMessage.Value ="Message Tracking Logs contains " + $script:MessageTrackingLogs.Count + " entries" + $WriteMessageAlways.Value = $true + return $true +} diff --git a/Calendar/BookingHelpers/BookingServiceHelpers.ps1 b/Calendar/BookingHelpers/BookingServiceHelpers.ps1 new file mode 100644 index 000000000..21a5ff0dc --- /dev/null +++ b/Calendar/BookingHelpers/BookingServiceHelpers.ps1 @@ -0,0 +1,80 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function Get-GraphBookingsServices { + param( + [Parameter(Mandatory = $true)] + [string]$Identity + ) + $GraphBookingBusiness = Get-MgBookingBusinessService -BookingBusinessId $Identity + $MBStaff = Get-MgBookingBusinessStaffMember -BookingBusinessId $Identity + $Services = @() + foreach ($Service in $GraphBookingBusiness) { + #determine serviceType + $ServiceType = "" + if ($Service.StaffMemberIds.Count -gt 1) { + $ServiceType = "N" + } else { + $ServiceType = "1" + } + $ServiceType+= ":" + if ($Service.MaximumAttendeesCount -gt 1) { + $ServiceType += "N" + } else { + $ServiceType += "1" + } + #compose ServiceStaffIds + $ServiceStaffIds = "" + foreach ($StaffId in $Service.StaffMemberIds) { + $ServiceStaffIds += $StaffId + "`r`n" + } + $StaffName1 = "" + $StaffName2 = "" + $StaffName3 = "" + $StaffName4 = "" + $StaffName5 = "" + for ($I = 0; $I -lt $Service.StaffMemberIds.Count; $I++) { + foreach ($StaffName in $MBStaff) { + if ($StaffName.Id -eq $Service.StaffMemberIds[$I]) { + switch ($I) { + 0 { $StaffName1 = $StaffName.AdditionalProperties["displayName"] } + 1 { $StaffName2 = $StaffName.AdditionalProperties["displayName"] } + 2 { $StaffName3 = $StaffName.AdditionalProperties["displayName"] } + 3 { $StaffName4 = $StaffName.AdditionalProperties["displayName"] } + 4 { $StaffName5 = $StaffName.AdditionalProperties["displayName"] } + } + } + } + } + + $Services += [PSCustomObject]@{ + Id = $Service.Id + ServiceType = $ServiceType + DisplayName = $Service.DisplayName + Description = $Service.Description + Duration = $Service.DefaultDuration + PreBuffer = $Service.PreBuffer + PostBuffer = $Service.PostBuffer + SchedulingPolicy = $Service.SchedulingPolicy | ConvertTo-Json -Depth 10 + StaffMemberIds = $ServiceStaffIds + StaffName1 = $StaffName1 + StaffName2 = $StaffName2 + StaffName3 = $StaffName3 + StaffName4 = $StaffName4 + StaffName5 = $StaffName5 + MaximumAttendeesCount = $Service.MaximumAttendeesCount + CustomQuestions = $Service.CustomQuestions | ConvertTo-Json -Depth 10 + DefaultReminders = $Service.DefaultReminders | ConvertTo-Json -Depth 10 + IsHiddenFromCustomers = $Service.IsHiddenFromCustomers + IsLocationOnline = $Service.IsLocationOnline + DefaultLocation = $Service.DefaultLocation | ConvertTo-Json -Depth 10 + Notes = $Service.Notes + LanguageTag = $Service.LanguageTag + CreatedDateTime = $Service.AdditionalProperties["createdDateTime"] + LastUpdatedDateTime = $Service.AdditionalProperties["lastUpdatedDateTime"] + Price = $Service.AdditionalProperties["price"] + Currency = $Service.AdditionalProperties["currency"] + } + } + return $Services +} diff --git a/Calendar/BookingHelpers/BookingStaffHelpers.ps1 b/Calendar/BookingHelpers/BookingStaffHelpers.ps1 new file mode 100644 index 000000000..69d7aed41 --- /dev/null +++ b/Calendar/BookingHelpers/BookingStaffHelpers.ps1 @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function Get-MBPermissions { + param($Identity) + # Get the Mailbox Permissions + $MBPermissions = Get-MailboxPermission -Identity $Identity -ErrorAction SilentlyContinue + return $MBPermissions +} + +function Get-MBRecipientPermissions { + param($Identity) + # Get the Mailbox Recipient Permissions + $MBRecipientPermissions = Get-RecipientPermission -Identity $Identity -ErrorAction SilentlyContinue + return $MBRecipientPermissions +} + +function CheckMyBaseOptionsRBACRole($Identity) { + $RBACRole = Get-ManagementRoleAssignment -RoleAssignee $Identity -Role "MyBaseOptions" | Select-Object -Property RoleAssignee, Role + return $RBACRole +} + +# This method is used to check if a staff member has Bookings Persisted Capabilities +function CheckPersistedCapabilities($Identity) { + $PC = Get-Mailbox -Identity $Identity | Select-Object -ExpandProperty PersistedCapabilities + return $PC +} + +function Get-GraphBookingsStaff { + param ( + [string]$Identity + ) + + $MBstaff = Get-MgBookingBusinessStaffMember -BookingBusinessId $Identity + $Staff = @() + foreach ($StaffMember in $MBstaff) { + $Staff += [PSCustomObject]@{ + Id = $StaffMember.Id + displayName = $StaffMember.AdditionalProperties["displayName"] + emailAddress = $StaffMember.AdditionalProperties["emailAddress"] + availabilityIsAffectedByPersonalCalendar = $StaffMember.AdditionalProperties["availabilityIsAffectedByPersonalCalendar"] + role = $StaffMember.AdditionalProperties["role"] + useBusinessHours = $StaffMember.AdditionalProperties["useBusinessHours"] + isEmailNotificationEnabled = $StaffMember.AdditionalProperties["isEmailNotificationEnabled"] + membershipStatus = $StaffMember.AdditionalProperties["membershipStatus"] + timeZone = $StaffMember.AdditionalProperties["timeZone"] + createdDateTime = $StaffMember.AdditionalProperties["createdDateTime"] + lastUpdatedDateTime = $StaffMember.AdditionalProperties["lastUpdatedDateTime"] + # workingHours is a complex object type to write to excel, so, storing as JSON for easier visualization + workingHours = $StaffMember.AdditionalProperties["workingHours"] | ConvertTo-Json -Depth 10 + } + } + + return $Staff +} diff --git a/Calendar/BookingHelpers/BookingStaffLogHelper.ps1 b/Calendar/BookingHelpers/BookingStaffLogHelper.ps1 new file mode 100644 index 000000000..e929e4d7e --- /dev/null +++ b/Calendar/BookingHelpers/BookingStaffLogHelper.ps1 @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function GetStaffMembershipLogs { + param( + [Parameter(Mandatory=$true)] + [string]$Identity + ) + Write-Verbose "Checking Membership logs for $Identity" + $Log = Export-MailboxDiagnosticLogs -Identity $Identity -ComponentName BookingStaffMembershipLog -ErrorAction SilentlyContinue + return $Log +} + +function GetMembershipLogArray { + Write-Verbose "Reading Membership logs for $Identity and converting as array" + $MembershipLogArray = Export-MailboxDiagnosticLogs -Identity $Identity -ComponentName BookingStaffMembershipLog -ErrorAction SilentlyContinue + $MembershipLogArray = createArrayFromStaffLog -log $Log.MailboxLog + return $MembershipLogArray +} + +function createArrayFromStaffLog { + param([string]$Log) + #A sample line from the log array is a single string with the following format: + #2/19/2024 7:49:37 PM:StaffId:LU37rKfE1kKgCZy/qlYLtQ==,Name:external test,MailboxGuid:fba85c5f-8a4f-45f5-8749-8385d0fea4cb,MembershipStatus:Active,Action:Deleted + #The log is split by new line and then each line is split by comma to get the values + $LogArray = $Log -split [Environment]::NewLine + + $ARetVal = @() + $LogArray | ForEach-Object { + if (-not $_.Trim().Length -eq 0) { + $Line = $_.Split(",") + $ARetVal += [PSCustomObject]@{ + Date = $Line[0] + StaffID = $Line[1] + Name = $Line[2] + MailboxGuid = $Line[3] + MemberShipStatus = $Line[4] + } + } + } + return $ARetVal +} + +function export-BookingStaffMembershipLog { + param( + [Parameter(Mandatory = $true)] + [string]$Identity + ) + + $ArrayFromStaffLog = createArrayFromStaffLog -log $script:BookingStaffMembershipLog.MailboxLog + + $MBStaffMembershipLogArray = @() + foreach ($Log in $script:BookingStaffMembershipLog.MailboxLog.Split("`r`n")) { + $MBStaffMembershipLogArray += [PSCustomObject]@{ + log = $Log + } + } + $ArrayFromStaffLog | Export-Excel -Path $Path -WorksheetName "Membership Log Array" -TableStyle Medium3 -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -Title "Staff Membership Log" +} diff --git a/Calendar/BookingHelpers/BookingStaffLogLogic.ps1 b/Calendar/BookingHelpers/BookingStaffLogLogic.ps1 new file mode 100644 index 000000000..c6f67e372 --- /dev/null +++ b/Calendar/BookingHelpers/BookingStaffLogLogic.ps1 @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function RunMBStaffLogValidation { + [ref]$ErrorMessage = $null + [ref]$WriteMessageAlways = $true + $TestResult = $true + + Write-DashLineBoxColor "Running Staff Membership Log collection" -Color Blue + + $ErrorMessage = "" + $TestResult = CheckBMBStaffMemberShipLog -errorMessage $ErrorMessage + WriteTestResult "Get Mailbox Staff membership logs" -success $TestResult -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways +} + +function CheckBMBStaffMemberShipLog { + param([ref]$ErrorMessage) + Write-Verbose "Checking Membership Staff Log for $Identity" + if ($null -eq $script:BookingStaffMembershipLog) { + $ErrorMessage.Value = "Staff Membership log not found." + return $false + } + + $ErrorMessage.Value = "Membership logs has " + $script:BookingStaffMembershipLogArray.Count + " entries" + return $true +} diff --git a/Calendar/BookingHelpers/BookingStaffLogic.ps1 b/Calendar/BookingHelpers/BookingStaffLogic.ps1 new file mode 100644 index 000000000..d77d83b6e --- /dev/null +++ b/Calendar/BookingHelpers/BookingStaffLogic.ps1 @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function Get-StaffData { + $StaffData = @() + + foreach ($StaffMember in $script:MBPermissions) { + if ($StaffMember.User -ne "NT AUTHORITY\SELF") { + $StaffData += [PSCustomObject]@{ + User = $StaffMember.User + AccessRights = $StaffMember.AccessRights + IsOwner = $StaffMember.IsOwner + RBACRole = CheckMyBaseOptionsRBACRole -identity $StaffMember.User + PersistedCapabilities = CheckPersistedCapabilities -identity $StaffMember.User + } + } + } + + return $StaffData +} diff --git a/Calendar/BookingHelpers/BookingTenantSettingsHelper.ps1 b/Calendar/BookingHelpers/BookingTenantSettingsHelper.ps1 new file mode 100644 index 000000000..3c59f1a14 --- /dev/null +++ b/Calendar/BookingHelpers/BookingTenantSettingsHelper.ps1 @@ -0,0 +1,107 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function GetBookingTenantSettings { + param([string] $Domain) + + if ($script:MSSupport) { + $script:OrgConfig = Get-OrganizationConfig -Organization $Domain + $script:OwaMailboxPolicy = Get-OwaMailboxPolicy -Organization $Domain + $script:AcceptedDomains = Get-AcceptedDomain -Organization $Domain + } else { + $script:OrgConfig = Get-OrganizationConfig + $script:OwaMailboxPolicy = Get-OwaMailboxPolicy + $script:AcceptedDomains = Get-AcceptedDomain + } + $EwsSettings = GetEWSSettings $script:OrgConfig + $BookingsSettings = $script:OrgConfig + $OwaMailboxPolicy = $script:OwaMailboxPolicy + $AcceptedDomains = GetAcceptedDomains $script:AcceptedDomains + # Define the structure of the tenant settings + $TenantSettings = [PSCustomObject]@{ + Identity = $Org.Identity + Guid = $Org.Guid + DisplayName = $Org.DisplayName + IsDeHydrated = $Org.IsDeHydrated + EWSSettings = $EwsSettings + BookingsSettings = $BookingsSettings + OwaMailboxPolicy = $OwaMailboxPolicy + AcceptedDomains = $AcceptedDomains + } + + # Return the tenant settings + return $TenantSettings +} + +function GetEWSSettings { + param($Org) + # Define the structure of the EWS settings + $EwsSettings = [PSCustomObject]@{ + EwsAllowList =$Org.EwsAllowList + EwsApplicationAccessPolicy =$Org.EwsApplicationAccessPolicy + EwsBlockList =$Org.EwsBlockList + EwsEnabled =$Org.EwsEnabled + } + + # Return the EWS settings + return $EwsSettings +} + +function GetBookingsSettings { + param($OrgConfig) + # Define the structure of the Bookings settings + $BookingsSettings = [PSCustomObject]@{ + BookingsEnabled =$OrgConfig.BookingsEnabled + BookingsEnabledLastUpdateTime =$OrgConfig.BookingsEnabledLastUpdateTime + BookingsPaymentsEnabled =$OrgConfig.BookingsPaymentsEnabled + BookingsSocialSharingRestricted =$OrgConfig.BookingsSocialSharingRestricted + BookingsAddressEntryRestricted =$OrgConfig.BookingsAddressEntryRestricted + BookingsAuthEnabled =$OrgConfig.BookingsAuthEnabled + BookingsCreationOfCustomQuestionsRestricted =$OrgConfig.BookingsCreationOfCustomQuestionsRestricted + BookingsExposureOfStaffDetailsRestricted =$OrgConfig.BookingsExposureOfStaffDetailsRestricted + BookingsNotesEntryRestricted =$OrgConfig.BookingsNotesEntryRestricted + BookingsPhoneNumberEntryRestricted =$OrgConfig.BookingsPhoneNumberEntryRestricted + BookingsMembershipApprovalRequired =$OrgConfig.BookingsMembershipApprovalRequired + BookingsSmsMicrosoftEnabled =$OrgConfig.BookingsSmsMicrosoftEnabled + BookingsNamingPolicyEnabled =$OrgConfig.BookingsNamingPolicyEnabled + BookingsBlockedWordsEnabled =$OrgConfig.BookingsBlockedWordsEnabled + BookingsNamingPolicyPrefixEnabled =$OrgConfig.BookingsNamingPolicyPrefixEnabled + BookingsNamingPolicyPrefix =$OrgConfig.BookingsNamingPolicyPrefix + BookingsNamingPolicySuffixEnabled =$OrgConfig.BookingsNamingPolicySuffixEnabled + BookingsNamingPolicySuffix =$OrgConfig.BookingsNamingPolicySuffix + BookingsSearchEngineIndexDisabled =$OrgConfig.BookingsSearchEngineIndexDisabled + IsTenantInGracePeriod =$OrgConfig.IsTenantInGracePeriod + IsTenantAccessBlocked =$OrgConfig.IsTenantAccessBlocked + IsDehydrated =$OrgConfig.IsDehydrated + ServicePlan =$OrgConfig.ServicePlan #check doc for service plans accepting Bookings4 + } + + # Return the Bookings settings + return $BookingsSettings +} + +function GetOwaMailboxPolicy { + param($Policy) + # Define the structure of the OWA mailbox policy + $OwaMailboxPolicy = [PSCustomObject]@{ + BookingsMailboxCreationEnabled = $Policy.BookingsMailboxCreationEnabled + BookingsMailboxDomain = $Policy.BookingsMailboxDomain + } + + # Return the OWA mailbox policy + return $OwaMailboxPolicy +} + +function GetAcceptedDomains { + param($Domains) + + # Define the structure of the accepted domains + $AcceptedDomains = [PSCustomObject]@{ + DomainName = $Domains.DomainName + Default = $Domains.Default + AuthenticationType = $Domains.AuthenticationType + } + + # Return the accepted domains + return $AcceptedDomains +} diff --git a/Calendar/BookingHelpers/BookingTenantSettingsLogic.ps1 b/Calendar/BookingHelpers/BookingTenantSettingsLogic.ps1 new file mode 100644 index 000000000..a3078df9d --- /dev/null +++ b/Calendar/BookingHelpers/BookingTenantSettingsLogic.ps1 @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function RunTenantTests { + [ref]$ErrorMessage = $null + [ref]$WriteMessageAlways = $false + $TestResult = $true + + Write-DashLineBoxColor "Running Tenant Tests" -Color Blue + + $ErrorMessage = "" + $TestResult = CheckBookingsEnabled -errorMessage $ErrorMessage + WriteTestResult "Bookings is enabled" -success $TestResult -errorMessage $ErrorMessage + + $TestResult = CheckAcceptedDomainIsManaged -errorMessage $ErrorMessage + WriteTestResult "Is Default domain Managed" -success $TestResult -errorMessage $ErrorMessage + + $TestResult = CheckEWSEnabled -errorMessage $ErrorMessage + WriteTestResult -title "Check EWS Enabled" -success $TestResult -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways + + $TestResult = CheckEWSAccessPolicy -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways + WriteTestResult -title "Check EWS Access Policy" -success $TestResult -errorMessage $ErrorMessage -writeMessageAlways $true + #dump allow/block list data below, indented + WriteEWSAllowList + WriteEWSBlockList + + $TestResult = CheckOWAMailboxPolicyMailboxCreationEnabled -errorMessage $ErrorMessage + WriteTestResult "Mailbox Creation is enabled" -success $TestResult -errorMessage $ErrorMessage + + $TestResult = CheckBookingsMBDomain -errorMessage $ErrorMessage + WriteTestResult "BookingsMailboxDomain in the correct domain" -success $TestResult -errorMessage $ErrorMessage + + $TestResult = CheckDomainSuffix -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways + WriteTestResult "DomainSuffix test for invalid chars" -success $TestResult -errorMessage $ErrorMessage -writeMessageAlways $WriteMessageAlways + return +} + +function CheckAcceptedDomainIsManaged { + param([ref]$ErrorMessage) + + Write-Verbose "Checking if accepted domain is managed" + $AcceptedDomains = $script:TenantSettings.AcceptedDomains + if ($null -eq $AcceptedDomains) { + $ErrorMessage.Value = "Accepted domains is null." + return $false + } + + if (@($AcceptedDomains | Where-Object { $_.AuthenticationType -eq "Managed" -and $_.DomainName -eq $script:Domain -and $_.Default -eq $true } ).Count -eq 0 ) { + $ErrorMessage.Value = "There is no Default Accepted domain, with Authentication type Managed. " + return $false + } + + if (@($AcceptedDomains | Where-Object { $_.Default -eq $true } ).Count -gt 1 ) { + $ErrorMessage.Value = "There is more than 1 Default Domain, make sure there is only one and AuthenticationType is Managed." + return $false + } + return $true +} + +function CheckBookingsEnabled { + param([ref]$ErrorMessage) + + Write-Verbose "Checking if Bookings is enabled for the tenant" + if ($script:TenantSettings.BookingsSettings.BookingsEnabled -ne $true) { + $ErrorMessage.Value = "Bookings is not enabled for the Tenant - Check Get-OrganizationConfig." + return $false + } + return $true +} + +function CheckEWSEnabled { + param([ref]$ErrorMessage, [ref]$WriteMessageAlways) + + Write-Verbose "Checking if EWS is enabled for the tenant" + if ($script:TenantSettings.EwsEnabled -ne $true -and $null -ne $script:TenantSettings.EwsEnabled) { + $ErrorMessage.Value = "EWS is not enabled for the Tenant - Check Get-OrganizationConfig." + return $false + } + return $true +} + +function CheckEWSAccessPolicy { + param([ref]$ErrorMessage, [ref]$WriteMessageAlways) + + Write-Verbose "Checking if EWS Access Policy is set" + + if ($null -eq $script:TenantSettings.EwsApplicationAccessPolicy) { + $ErrorMessage.Value = "EWS Application Access Policy is not set." + $WriteMessageAlways.Value = $true + return $true + } + if ($script:TenantSettings.EwsApplicationAccessPolicy -eq "EnforceAllowList" -or $script:TenantSettings.EwsApplicationAccessPolicy -eq "EnforceBlockList") { + $ErrorMessage.Value = "EWS Application Access Policy is set to " + $script:TenantSettings.EwsApplicationAccessPolicy + $WriteMessageAlways.Value = $true + } + + return $true +} + +function WriteEWSAllowList { + if ($script:TenantSettings.EwsApplicationAccessPolicy -eq "EnforceAllowList") { + $script:TenantSettings.EwsAllowList | ForEach-Object { Write-Output "$script:indent$_" } + } +} + +function WriteEWSBlockList { + if ($script:TenantSettings.EwsApplicationAccessPolicy -eq "EnforceBlockList") { + $script:TenantSettings.EwsBlockList | ForEach-Object { Write-Output "$script:indent$_" } + } +} + +function CheckOWAMailboxPolicyMailboxCreationEnabled { + param([ref]$ErrorMessage) + + Write-Verbose "Checking if Mailbox Creation is enabled for the tenant" + if ($script:TenantSettings.OwaMailboxPolicy.BookingsMailboxCreationEnabled -ne $true) { + $ErrorMessage.Value = "Check Get-OwaMailboxPolicy" + return $false + } + return $true +} + +function CheckBookingsMBDomain { + param([ref]$ErrorMessage) + + Write-Verbose "Checking if Bookings Mailbox is in the correct domain" + if ($script:Domain -ne $script:OwaMailboxPolicy.BookingsMailboxDomain) { + $ErrorMessage.Value = "Get-OWAMailboxPolicy BookingsMailboxDomain is " + $script:OwaMailboxPolicy.BookingsMailboxDomain + return $false + } + return $true +} + +function CheckDomainSuffix { + param([ref]$ErrorMessage, [ref]$WriteMessageAlways) + + Write-Verbose "Checking if domain suffix is correct" + if ($script:OwaMailboxPolicy.DomainSuffix -match '[^a-zA-Z0-9]') { + $ErrorMessage.Value = "OwaMailboxPolicy Bookings DomainSuffix may have invalid chars " + $script:OwaMailboxPolicy.DomainSuffix + return $false + } + $ErrorMessage.Value="No invalid characters were found." + $WriteMessageAlways.Value = $true + return $true +} + +function CheckSharingPolicy { + param([ref]$ErrorMessage) + + Write-Verbose "Checking if Sharing Policy is set" + if ($null -eq $script:TenantSettings.BookingsSettings.BookingsSocialSharingRestricted) { + $ErrorMessage.Value = "Bookings Social Sharing Policy is not set." + return $false + } + return $true +} diff --git a/Calendar/BookingHelpers/ExcelWrite.ps1 b/Calendar/BookingHelpers/ExcelWrite.ps1 new file mode 100644 index 000000000..29492bda9 --- /dev/null +++ b/Calendar/BookingHelpers/ExcelWrite.ps1 @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# ======================== +# Excel needs Scriptlets helper functions to be loaded, in order to have access to graph and install Export-Excel module. +# ======================== + +. $PSScriptRoot\GraphInstaller.ps1 +. $PSScriptRoot\BookingGenericFunctions.ps1 +. $PSScriptRoot\BookingMBHelpers.ps1 +. $PSScriptRoot\BookingStaffHelpers.ps1 +. $PSScriptRoot\BookingServiceHelpers.ps1 +. $PSScriptRoot\BookingCustomQuestionHelpers.ps1 +. $PSScriptRoot\BookingStaffLogHelper.ps1 + +function GetExcelParams($Path, $TabName) { + + return @{ + Path = $Path + FreezeTopRow = $true + Verbose = $false + TableStyle = "Medium3" + WorksheetName = $TabName + TableName = $TabName + FreezeTopRowFirstColumn = $false + AutoFilter = $true + AutoNameRange = $true + Append = $true + AutoSize = $true + Title = "Bookings Support Data Collector" + ConditionalText = $ConditionalFormatting + } +} + +function CheckModulesAndConnectGraph { + Write-Host -ForegroundColor Gray "Checking Excel, Graph and bookings modules" + CheckExcelModuleInstalled + CheckGraphAuthModuleInstalled + CheckGraphBookingsModuleInstalled + + $ModulesInstalled = CheckGraphModulesInstalled + Write-Verbose "Modules installed: $ModulesInstalled" + if ($ModulesInstalled -eq $false) { + Write-Host -ForegroundColor Red "Graph modules are not installed. Exiting..." + exit + } + + Connect-MgGraph -Scopes "User.Read.All", "Bookings.Read.All" -NoWelcome +} + +function ExcelWrite { + param($Identity) + + CheckModulesAndConnectGraph + + Write-Host -ForegroundColor Green "Exporting to Excel..." + + #Compose File name formatted as: BookingsSummary___.xlsx + $IdentityName = SplitIdentityFromEmail -email $Identity + $DateStringToFileName = (Get-Date).ToString("yyyy-MM-dd") + $HourStringToFileName = Get-Date -Format "HHmm" + $Path = "BookingsSummary_$($IdentityName)_$($DateStringToFileName)_$($HourStringToFileName).xlsx" + Remove-Item -Path $Path -Force -EA Ignore + + $ExcelParamsArray = GetExcelParams -path $Path -tabName "Business" + + Write-Host "Exporting Booking Business." + $BusinessData = GetGraphBookingBusiness -Identity $Identity + $Excel = $BusinessData | Export-Excel @ExcelParamsArray -PassThru + Export-Excel -ExcelPackage $Excel -WorksheetName "Business" -MoveToStart + + Write-Host "Exporting Bookings Page Settings." + $BusinessPage = GetGraphBookingBusinessPage -Identity $Identity + $BusinessPage | Export-Excel -Path $Path -WorksheetName "Business" -StartRow 10 -Title "Page Settings" -TableStyle Medium3 -AutoSize + + Write-Host "Exporting Booking Policy." + $BookingPolicy = GetGraphBookingBusinessBookingPolicy -Identity $Identity + $BookingPolicy | Export-Excel -Path $Path -WorksheetName "Business" -BoldTopRow -StartRow 10 -StartColumn 10 -Title "Scheduling Policy" -TableStyle Medium3 -AutoSize + + Write-Host "Exporting Working Hours." + $WorkingHours = GetGraphBookingBusinessWorkingHours -Identity $Identity + $WorkingHours | Export-Excel -Path $Path -WorksheetName "Business" -BoldTopRow -StartRow 25 -Title "Working Hours" -TableStyle Medium24 -AutoSize + + Write-Host "Exporting Staff." + $Staff = Get-GraphBookingsStaff -Identity $Identity + $Staff | Export-Excel -Path $Path -WorksheetName "Staff" -TableStyle Medium3 -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -Title "Staff List" + + Write-Host "Exporting Services." + $Services = Get-GraphBookingsServices -Identity $Identity + $Services | Export-Excel -Path $Path -WorksheetName "Services" -TableStyle Medium3 -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -Title "Services" + + Write-Host "Exporting Custom Questions." + $CustomQuestions = Get-GraphBookingsCustomQuestions -Identity $Identity + $CustomQuestions | Export-Excel -Path $Path -WorksheetName "Custom Questions" -TableStyle Medium3 -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -Title "Custom Questions" + + Write-Host "Exporting MessageTrace for the past $($script:MessageTrackingDays) days." + $script:MessageTrackingLogs | Export-Excel -Path $Path -WorksheetName "Message Traces" -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -TableStyle Medium3 + + Write-Host "Exporting Membership Log data to Excel." + $ArrayFromStaffLog = createArrayFromStaffLog -log $script:BookingStaffMembershipLog.MailboxLog + $ArrayFromStaffLog | Export-Excel -Path $Path -WorksheetName "Membership Log" -TableStyle Medium3 -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -Title "Staff Membership Log" + + Write-Host "Exporting Internal data to Excel." + $script:TenantSettings | Export-Excel -Path $Path -WorksheetName "Tenant Settings - Internal" -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -TableStyle Medium3 #-Title "Tenant Settings - Internal" + $script:BookingMBData | Export-Excel -Path $Path -WorksheetName "Mailbox Data - Internal" -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -TableStyle Medium3 #-Title "Mailbox Data - Internal" + $script:MBPermissions | Export-Excel -Path $Path -WorksheetName "MB Permissions-Internal" -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -TableStyle Medium3 #-Title "MB Permissions - Internal" + $script:MBRecipientPermissions | Export-Excel -Path $Path -WorksheetName "Recipient Permissions-Internal" -AutoFilter -FreezeTopRow -BoldTopRow -MoveToEnd -AutoSize -TableStyle Medium3 #-tableTitle "Recipient Permissions - Internal" + + Write-Host -ForegroundColor Green "Exporting to Excel Completed. File saved at $Path" +} diff --git a/Calendar/BookingHelpers/FileSaveHelpers.ps1 b/Calendar/BookingHelpers/FileSaveHelpers.ps1 new file mode 100644 index 000000000..937816bca --- /dev/null +++ b/Calendar/BookingHelpers/FileSaveHelpers.ps1 @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function BaseFileName([string]$Identity, [string]$Suffix) { + $Smtp = $Identity.Split('@')[0] + $Smtp = $Smtp -replace '[\\/:*?"<>|]', '' + $Date = Get-Date -Format "yyyyMMdd" + $Hour = Get-Date -Format "HHmm" + if ([string]::IsNullOrEmpty($Suffix)) { + $File = $Date + "_" + $Hour + "_" + $Smtp + } else { + $File = $Date + "_" + $Hour + "_" + $Smtp + "_" + $Suffix + } + return $File +} + +function CSVFilename([string]$Identity, [string]$Suffix) { + return (BaseFileName $Identity $Suffix) + ".csv" +} + +function XLSXFilename([string]$Identity) { + return (BaseFileName $Identity $Suffix) + ".xlsx" +} + +function SaveDataAsCSV([string]$Identity) { + $Filename = CSVFilename -Identity $Identity -suffix "TenantSettings" + $script:TenantSettings | Export-Csv -Path $Filename -NoTypeInformation -ErrorAction SilentlyContinue + + $Filename = CSVFilename -identity $Identity -suffix "MBData" + $script:BookingMBData | Export-Csv -Path $Filename -NoTypeInformation -ErrorAction SilentlyContinue + + $Filename = CSVFilename -identity $Identity -suffix "StaffMembershipLog" + $script:BookingStaffMembershipLogArray | Export-Csv -Path $Filename -NoTypeInformation -ErrorAction SilentlyContinue + + $Filename = CSVFilename -identity $Identity -suffix "MTLogs" + $script:MessageTrackingLogs | Export-Csv -Path $Filename -NoTypeInformation -ErrorAction SilentlyContinue + + $Filename = CSVFilename -identity $Identity -suffix "StaffData" + $script:StaffData | Export-Csv -Path $Filename -NoTypeInformation -ErrorAction SilentlyContinue +} diff --git a/Calendar/BookingHelpers/GraphInstaller.ps1 b/Calendar/BookingHelpers/GraphInstaller.ps1 new file mode 100644 index 000000000..dceceb1e5 --- /dev/null +++ b/Calendar/BookingHelpers/GraphInstaller.ps1 @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# =================================================================================================== +# Graph Modules installer Functions +# =================================================================================================== +function CheckGraphBookingsModuleInstalled { + [CmdletBinding(SupportsShouldProcess=$true)] + param () + + if (Get-Command -Module Microsoft.Graph.Bookings) { + Write-Host "Microsoft.Graph.Bookings module is already installed." + } else { + # This is slow, to the tune of ~10 seconds, but much more complete. + # Check if Microsoft.Graph.Bookings module is installed + $ModuleInstalled = Get-Module -ListAvailable | Where-Object { $_.Name -eq 'Microsoft.Graph.Bookings' } + + if ($ModuleInstalled) { + Write-Host "Microsoft.Graph.Bookings module is already installed." + } else { + # Check if running with administrator rights + if (-not $script:IsAdministrator) { + Write-Host "Please run the script as an administrator to install the Microsoft.Graph.Bookings module." + exit + } + + # Ask user if they want to install the module + if ($PSCmdlet.ShouldProcess('Microsoft.Graph.Bookings module', 'Import')) { + Write-Verbose "Installing Microsoft.Graph.Bookings module..." + + # Install Microsoft.Graph.Bookings module + Install-Module -Name Microsoft.Graph.Bookings -Force -AllowClobber + + Write-Host "Done. Microsoft.Graph.Bookings module is now installed. Please re-run the script." + } + } + } +} + +function CheckGraphAuthModuleInstalled { + [CmdletBinding(SupportsShouldProcess=$true)] + param () + + if (Get-Command -Module Microsoft.Graph.Authentication) { + Write-Host "Microsoft.Graph.Authentication module is already installed." + } else { + # This is slow, to the tune of ~10 seconds, but much more complete. + # Check if Microsoft.Graph.Authentication module is installed + $ModuleInstalled = Get-Module -ListAvailable | Where-Object { $_.Name -eq 'Microsoft.Graph.Authentication' } + + if ($ModuleInstalled) { + Write-Host "Microsoft.Graph.Authentication module is already installed." + } else { + # Check if running with administrator rights + if (-not $script:IsAdministrator) { + Write-Host "Please run the script as an administrator to install the Microsoft.Graph.Authentication module." + exit + } + + # Ask user if they want to install the module + if ($PSCmdlet.ShouldProcess('Microsoft.Graph.Authentication module', 'Import')) { + Write-Verbose "Installing Microsoft.Graph.Authentication module..." + + # Install Microsoft.Graph.Authentication module + Install-Module -Name Microsoft.Graph.Authentication -Force -AllowClobber + + Write-Host "Done. Microsoft.Graph.Authentication module is now installed. Please re-run the script." + } + } + } +} + +function CheckGraphModulesInstalled { + + $ModuleAuthInstalled = ($null -ne (Get-Command -Module Microsoft.Graph.Authentication)) + $ModuleBookingsInstalled = ($null -ne (Get-Command -Module Microsoft.Graph.Bookings)) + + Write-Verbose "Microsoft.Graph.Authentication module installed: $ModuleAuthInstalled" + Write-Verbose "Microsoft.Graph.Bookings module installed: $ModuleBookingsInstalled" + + return [bool]($ModuleAuthInstalled -and $ModuleBookingsInstalled) +} diff --git a/Calendar/BookingsDiagnosticSummary.ps1 b/Calendar/BookingsDiagnosticSummary.ps1 new file mode 100644 index 000000000..e675f6886 --- /dev/null +++ b/Calendar/BookingsDiagnosticSummary.ps1 @@ -0,0 +1,149 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# .DESCRIPTION +# This Exchange Online script runs the Get-BookingsDiagnosticSummary script and returns a summary of basic bookings tests on +# a selected Bookings mailbox. The script will return the Bookings Mailbox, Staff, Services, Custom Questions, +# as well as MessageTrace logs for the past 5 days. +# The script will also return the Staff Membership Log for the Bookings mailbox. +# +# .PARAMETER Identity +# The Bookings mailbox SMTP address to query. (Mandatory) +# +# .PARAMETER Staff +# Verify Staff permissions for the Bookings mailbox. +# +# .PARAMETER StaffMembershipLog +# Get the Staff Membership Log for the Bookings mailbox. +# +# .PARAMETER Graph +# Use Graph API to get the Bookings mailbox, Staff, Services and Availability. +# This will require the Microsoft.Graph.Bookings and Microsoft.Graph.Authentication modules to be installed. +# (For the first run, the script will install the modules if they are not already installed, +# for this you need to run the script as an administrator) +# +# .PARAMETER GetMessageTrace +# Get MessageTrace logs for the Bookings mailbox(Past 5 days). +# +# .PARAMETER ExportExcel +# Export all data to Excel. +# This will require the ImportExcel module to be installed. +# (For the first run, the script will install the module if not already installed, +# for this you need to run the script as an administrator) +# +# .EXAMPLE +# Get-BookingsDiagnosticSummary.ps1 -Identity fooBooking@microsoft.com +# +param +( + [Parameter(Position=0, Mandatory=$true, HelpMessage="Specifies the Bookings mailbox to be accessed.")] + [string]$Identity, + + [Parameter(Position=1, Mandatory=$False, HelpMessage="Verify Staff permissions for the Bookings mailbox.")] + [bool]$Staff=$true, + + [Parameter(Position=2, Mandatory=$False, HelpMessage="Get the Staff Membership Log for the Bookings mailbox.")] + [bool]$StaffMembershipLog = $true, + + [Parameter(Position=3, Mandatory=$False, HelpMessage="Use Graph API to get the Bookings mailbox, Staff, Services and Availability.")] + [bool]$Graph = $true, + + [Parameter(Position=4, Mandatory=$False, HelpMessage="Get MessageTrace logs for the Bookings mailbox(Past 5 days).")] + [bool]$MessageTrace = $true, + + [Parameter(Position=5, Mandatory=$False, HelpMessage="Export all data to CSV.")] + [bool]$ExportToCSV = $true, + + [Parameter(Position=6, Mandatory=$False, HelpMessage="Export all data to Excel.")] + [bool]$ExportToExcel = $true + +) + +# =================================================================================================== +# Auto update script +# =================================================================================================== +$BuildVersion = "" +. $PSScriptRoot\..\Shared\ScriptUpdateFunctions\Test-ScriptVersion.ps1 +if (Test-ScriptVersion -AutoUpdate -Confirm:$false) { + # Update was downloaded, so stop here. + Write-Host "Script was updated. Please rerun the command." -ForegroundColor Yellow + return +} + +Write-Verbose "Script Versions: $BuildVersion" + +# =================================================================================================== +# Helper scripts +# =================================================================================================== +. $PSScriptRoot\BookingHelpers\BookingGenericFunctions.ps1 +. $PSScriptRoot\BookingHelpers\BookingTenantSettingsHelper.ps1 +. $PSScriptRoot\BookingHelpers\BookingTenantSettingsLogic.ps1 +. $PSScriptRoot\BookingHelpers\BookingMBHelpers.ps1 +. $PSScriptRoot\BookingHelpers\BookingMBLogic.ps1 +. $PSScriptRoot\BookingHelpers\BookingMessageTrackingLogHelper.ps1 +. $PSScriptRoot\BookingHelpers\BookingMessageTrackingLogLogic.ps1 +. $PSScriptRoot\BookingHelpers\BookingStaffHelpers.ps1 +. $PSScriptRoot\BookingHelpers\BookingStaffLogic.ps1 +. $PSScriptRoot\BookingHelpers\BookingStaffLogHelper.ps1 +. $PSScriptRoot\BookingHelpers\BookingStaffLogLogic.ps1 +. $PSScriptRoot\BookingHelpers\FileSaveHelpers.ps1 +. $PSScriptRoot\BookingHelpers\ExcelWrite.ps1 +. $PSScriptRoot\CalLogHelpers\Write-DashLineBoxColor.ps1 +. $PSScriptRoot\CalLogHelpers\ExcelModuleInstaller.ps1 +. $PSScriptRoot\..\Shared\Confirm-Administrator.ps1 + +# Populating Global Variables and getting general data + +# See if it is a Customer Tenant running the cmdlet. (They will not have access to Organization parameter) +$script:MSSupport = [Bool](Get-Help Get-Mailbox -Parameter Organization -ErrorAction SilentlyContinue) +Write-Verbose "MSSupport: $script:MSSupport" + +$script:IsAdministrator = Confirm-Administrator + +$script:PadCharsMessage = 40 +$script:indent = " " +$script:MessageTrackingDays = 5 + +CheckEXOConnection + +$script:Identity = $Identity +$script:Domain = SplitDomainFromEmail $Identity -errorAction SilentlyContinue +$script:OrgConfig="" +$script:OwaMailboxPolicy = "" +$script:AcceptedDomains = "" +$script:TenantSettings = GetBookingTenantSettings -Domain $script:Domain -ErrorAction SilentlyContinue + +$script:BookingMBData = GetBookingMBData -Identity $Identity -ErrorAction SilentlyContinue +if ($StaffMembershipLog -eq $true) { + $script:BookingStaffMembershipLog = GetStaffMembershipLogs -Identity $Identity +} + +$script:BookingStaffMembershipLogArray = GetMembershipLogArray -Identity $Identity +$script:MessageTrackingLogs = GetMessageTrackingLog -identity $Identity -ErrorAction SilentlyContinue + +$script:MBPermissions = Get-MBPermissions -Identity $Identity -ErrorAction SilentlyContinue +$script:MBRecipientPermissions = Get-MBRecipientPermissions -Identity $Identity -ErrorAction SilentlyContinue +$script:StaffData = Get-StaffData | Format-Table + +# =================================================================================================== +# Start verifications +# =================================================================================================== +RunTenantTests +RunMBTests -Identity $Identity +if ($StaffMembershipLog -eq $true) { + RunMBStaffLogValidation +} + +if ($MessageTrace -eq $true) { + RunMessageTrackingLogValidation +} + +# Start data collection +if ($ExportToCSV -eq $true) { + saveDataAsCSV -Identity $Identity +} + +if ($ExportToExcel -eq $true) { + ExcelWrite -identity $Identity +} diff --git a/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 b/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 index b04a2b449..b80f72772 100644 --- a/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 +++ b/Calendar/CalLogHelpers/ExcelModuleInstaller.ps1 @@ -20,9 +20,7 @@ function CheckExcelModuleInstalled { Write-Host "ImportExcel module is already installed." } else { # Check if running with administrator rights - $isAdministrator = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - - if (-not $isAdministrator) { + if (-not $script:IsAdministrator) { Write-Host "Please run the script as an administrator to install the ImportExcel module." exit } diff --git a/docs/Calendar/BookingsDiagnosticSummary.md b/docs/Calendar/BookingsDiagnosticSummary.md new file mode 100644 index 000000000..0421490a5 --- /dev/null +++ b/docs/Calendar/BookingsDiagnosticSummary.md @@ -0,0 +1,68 @@ +# BookingsDiagnosticSummary + +Download the latest release: [BookingsDiagnosticSummary.ps1](https://aka.ms/BookingsDiagnosticSummary) + +This script runs a series of tests in a bookings Mailbox (one per execution) and returns a summarized list of the bookings Mailbox characteristics, as well as testing for known configuration issues that can lead to bookings not performing as expected. + +This script only runs on Exchange Online, as Microsoft Bookings is an online only application. + +Additionally, it will collect the most common logs needed for troubleshooting by support, including: + +* Staff Membership log +* Message Tracking Log +* Booking Mailbox configuration +* Staff List and Permissions +* Services configuration + +To run the script, you will need a valid SMTP Address for a booking Mailbox. + +The Identity parameter is required, all remaining are optional and default to true. + + +## Syntax + +```powershell +BookingsDiagnosticSummary.ps1 -Identity + [-Staff ] + [-StaffMembershipLog ] + [-Graph ] + [-MessageTrace ] + [-ExportToCSV ] + [-ExportToExcel ] +``` + + +| Parameters: | Explanation: | +| :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **-Identity** | Booking MB SMTP Address (Only one per execution) | +| **-Staff** | Verify Staff permissions for the Bookings mailbox. | +| **-StaffMemberShipLog** | Get the Staff Membership Log for the Bookings mailbox. | +| **-Graph** | Use Graph API to get the Bookings mailbox, Staff, Services and Availability.
Graph will allow the best comprehensive tests going through, as it will collect services data and staff, allowing to check more issues, such as permissions and more.
In the graph connection you will need the following scopes (Delegated):
User.Read.All
Bookings.Read.All | +| **-MessageTrace** | Get MessageTrace logs for the Bookings mailbox(Past 5 days). | +| **-ExportToCSV** | Export all data to CSV. | +| **-ExportToExcel** | Export the output to an Excel file with formatting. | + +--- + +#### Examples: + +Example to perform all tests on a Bookings Mailbox: + +```PowerShell +BookingsDiagnosticSummary.ps1 -Identity booking@contoso.com +``` + +Example to perform tests without collecting Message Traces: + +```PowerShell +BookingsDiagnosticSummary.ps1 -Identity booking@contoso.com -MessageTrace $false +``` + +Export test results to Excel, but skip CSV files creation: + +```PowerShell +BookingsDiagnosticSummary.ps1 -Identity booking@contoso.com -ExportCSV $false +``` + +Will create file like `.\BookingsSummary__yyyy-MM-dd_HHmm.xlsx` in current directory. +`` will be the left part of the @ from the email. I.e. booking@contoso.com returns booking. diff --git a/mkdocs.yml b/mkdocs.yml index 94dab32f5..8dbf71e05 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ nav: - Check-SharingStatus: Calendar/Check-SharingStatus.md - Get-CalendarDiagnosticObjectsSummary: Calendar/Get-CalendarDiagnosticObjectsSummary.md - Get-RBASummary: Calendar/Get-RBASummary.md + - BookingsDiagnosticSummary: Calendar/BookingsDiagnosticSummary.md - Databases: - Analyze-SpaceDump: Databases/Analyze-SpaceDump.md - Compare-MailboxStatistics: Databases/Compare-MailboxStatistics.md