From ec9816142fe6b9867de55f8f8e6fddcb213f6580 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:56:54 +0800 Subject: [PATCH 01/11] Alternate email address fix This was blocking submission of the edit user page, also its a comma separated list so corrected backend apis Tested with new users with 0,1 and 2 emails set as alternatives in both the user creator and the user editor pages --- .../Administration/Users/Invoke-EditUser.ps1 | 13 ++++++++++++- Modules/CIPPCore/Public/New-CippUser.ps1 | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index b3e91b0b4bc1..2c631f69154f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -32,6 +32,17 @@ function Invoke-EditUser { try { Write-Host "$([boolean]$UserObj.MustChangePass)" $UserPrincipalName = "$($UserObj.username)@$($UserObj.Domain ? $UserObj.Domain : $UserObj.primDomain.value)" + $normalizedOtherMails = @( + @($UserObj.otherMails) | ForEach-Object { + if ($null -ne $_) { + [string]$_ -split ',' + } + } | ForEach-Object { + $_.Trim() + } | Where-Object { + -not [string]::IsNullOrWhiteSpace($_) + } + ) $BodyToship = [pscustomobject] @{ 'givenName' = $UserObj.givenName 'surname' = $UserObj.surname @@ -49,7 +60,7 @@ function Invoke-EditUser { 'country' = $UserObj.country 'companyName' = $UserObj.companyName 'businessPhones' = $UserObj.businessPhones ? @($UserObj.businessPhones) : @() - 'otherMails' = $UserObj.otherMails ? @($UserObj.otherMails) : @() + 'otherMails' = $normalizedOtherMails 'passwordProfile' = @{ 'forceChangePasswordNextSignIn' = [bool]$UserObj.MustChangePass } diff --git a/Modules/CIPPCore/Public/New-CippUser.ps1 b/Modules/CIPPCore/Public/New-CippUser.ps1 index 7381e1426548..37b8ff9d70c0 100644 --- a/Modules/CIPPCore/Public/New-CippUser.ps1 +++ b/Modules/CIPPCore/Public/New-CippUser.ps1 @@ -16,6 +16,17 @@ function New-CIPPUser { $UserPrincipalName = "$($UserObj.username)@$($UserObj.Domain ? $UserObj.Domain : $UserObj.PrimDomain.value)" Write-Host "Creating user $UserPrincipalName" Write-Host "tenant filter is $($UserObj.tenantFilter)" + $normalizedOtherMails = @( + @($UserObj.otherMails) | ForEach-Object { + if ($null -ne $_) { + [string]$_ -split ',' + } + } | ForEach-Object { + $_.Trim() + } | Where-Object { + -not [string]::IsNullOrWhiteSpace($_) + } + ) $BodyToship = [pscustomobject] @{ 'givenName' = $UserObj.givenName 'surname' = $UserObj.surname @@ -25,7 +36,7 @@ function New-CIPPUser { 'mailNickname' = $UserObj.username ? $UserObj.username : $UserObj.mailNickname 'userPrincipalName' = $UserPrincipalName 'usageLocation' = $UserObj.usageLocation.value ? $UserObj.usageLocation.value : $UserObj.usageLocation - 'otherMails' = $UserObj.otherMails ? @($UserObj.otherMails) : @() + 'otherMails' = $normalizedOtherMails 'jobTitle' = $UserObj.jobTitle 'mobilePhone' = $UserObj.mobilePhone 'streetAddress' = $UserObj.streetAddress From d8c2c563b7cbc7b35b266343e5913bd9fc9df968 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 08:54:35 +0000 Subject: [PATCH 02/11] Add mailbox forwarding report endpoints Introduce Invoke-ListMailboxForwarding HTTP entrypoint and Get-CIPPMailboxForwardingReport report function. The entrypoint parses request params (CIPPEndpoint, tenantFilter, ForwardingOnly), calls the report function, logs activity and returns HttpResponseContext with appropriate status codes. The report generator reads cached mailbox data via Get-CIPPDbItem, supports a TenantFilter and -ForwardingOnly switch, handles 'AllTenants' by aggregating per-tenant reports, computes forwarding status (External/Internal/Both/None), and returns PSCustomObjects with fields like UPN, DisplayName, PrimarySmtpAddress, ForwardingType, ForwardTo, DeliverToMailboxAndForward, Tenant and CacheTimestamp. Error handling and logging added for missing data and per-tenant failures. --- .../Reports/Invoke-ListMailboxForwarding.ps1 | 49 +++++++ .../Get-CIPPMailboxForwardingReport.ps1 | 131 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 create mode 100644 Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 new file mode 100644 index 000000000000..4e54faabc185 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 @@ -0,0 +1,49 @@ +function Invoke-ListMailboxForwarding { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter + $ForwardingOnly = $Request.Query.ForwardingOnly + + try { + # Call the report function with proper parameters + $ReportParams = @{ + TenantFilter = $TenantFilter + } + if ($ForwardingOnly -eq 'true') { + $ReportParams.ForwardingOnly = $true + } + + try { + $GraphRequest = Get-CIPPMailboxForwardingReport @ReportParams + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Mailbox forwarding report listed for $($TenantFilter)" -sev Debug + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) +} diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 new file mode 100644 index 000000000000..312a29c81d08 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 @@ -0,0 +1,131 @@ +function Get-CIPPMailboxForwardingReport { + <# + .SYNOPSIS + Generates a mailbox forwarding report from the CIPP Reporting database + + .DESCRIPTION + Retrieves mailbox forwarding settings for a tenant from the cached mailbox data. + Shows mailboxes that have external forwarding, internal forwarding, or both configured. + + .PARAMETER TenantFilter + The tenant to generate the report for + + .PARAMETER ForwardingOnly + If specified, only returns mailboxes that have forwarding configured + + .EXAMPLE + Get-CIPPMailboxForwardingReport -TenantFilter 'contoso.onmicrosoft.com' + Gets all mailboxes with their forwarding settings + + .EXAMPLE + Get-CIPPMailboxForwardingReport -TenantFilter 'contoso.onmicrosoft.com' -ForwardingOnly + Gets only mailboxes that have forwarding configured + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [switch]$ForwardingOnly + ) + + try { + Write-LogMessage -API 'MailboxForwardingReport' -tenant $TenantFilter -message 'Generating mailbox forwarding report' -sev Debug + + # Handle AllTenants + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have mailbox data + $AllMailboxItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Mailboxes' + $Tenants = @($AllMailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPMailboxForwardingReport -TenantFilter $Tenant -ForwardingOnly:$ForwardingOnly + foreach ($Result in $TenantResults) { + # Add Tenant property to each result + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'MailboxForwardingReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + # Get mailboxes from reporting DB + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + if (-not $MailboxItems) { + throw 'No mailbox data found in reporting database. Sync the mailbox data first.' + } + + # Get the most recent cache timestamp + $CacheTimestamp = ($MailboxItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse mailbox data and build report + $Report = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $MailboxItems) { + $Mailbox = $Item.Data | ConvertFrom-Json + + # Determine forwarding status + $HasExternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.ForwardingSmtpAddress) + $HasInternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.InternalForwardingAddress) + $HasAnyForwarding = $HasExternalForwarding -or $HasInternalForwarding + + # Skip mailboxes without forwarding if ForwardingOnly is specified + if ($ForwardingOnly -and -not $HasAnyForwarding) { + continue + } + + # Determine forwarding type for display + $ForwardingType = if ($HasExternalForwarding -and $HasInternalForwarding) { + 'Both' + } elseif ($HasExternalForwarding) { + 'External' + } elseif ($HasInternalForwarding) { + 'Internal' + } else { + 'None' + } + + # Build the forward-to address display + $ForwardTo = if ($HasExternalForwarding) { + $Mailbox.ForwardingSmtpAddress + } elseif ($HasInternalForwarding) { + $Mailbox.InternalForwardingAddress + } else { + $null + } + + $Report.Add([PSCustomObject]@{ + UPN = $Mailbox.UPN + DisplayName = $Mailbox.displayName + PrimarySmtpAddress = $Mailbox.primarySmtpAddress + RecipientTypeDetails = $Mailbox.recipientTypeDetails + ForwardingType = $ForwardingType + ForwardTo = $ForwardTo + ForwardingSmtpAddress = $Mailbox.ForwardingSmtpAddress + InternalForwardingAddress = $Mailbox.InternalForwardingAddress + DeliverToMailboxAndForward = $Mailbox.DeliverToMailboxAndForward + HasForwarding = $HasAnyForwarding + Tenant = $TenantFilter + CacheTimestamp = $CacheTimestamp + }) + } + + # Sort by display name + $Report = $Report | Sort-Object -Property DisplayName + + Write-LogMessage -API 'MailboxForwardingReport' -tenant $TenantFilter -message "Generated forwarding report with $($Report.Count) entries" -sev Debug + return $Report + + } catch { + Write-LogMessage -API 'MailboxForwardingReport' -tenant $TenantFilter -message "Failed to generate mailbox forwarding report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw "Failed to generate mailbox forwarding report: $($_.Exception.Message)" + } +} From 9f51668f0b241a853f0163191240437199256601 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 09:19:25 +0000 Subject: [PATCH 03/11] Support UseReportDB and live EXO forwarding report Add UseReportDB query handling to fetch mailbox forwarding from the report DB (Get-CIPPMailboxForwardingReport). If UseReportDB=true the report function is called and returned; otherwise the code performs a live Exchange Online query (Get-Mailbox via New-ExoRequest), selects relevant fields and projects normalized PSCustomObjects with ForwardingType, ForwardTo, HasForwarding, and related properties. Also improve logging and error handling for both paths. --- .../Reports/Invoke-ListMailboxForwarding.ps1 | 86 +++++++++++++++---- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 index 4e54faabc185..836b1ff8d3a5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 @@ -10,31 +10,83 @@ function Invoke-ListMailboxForwarding { $APIName = $Request.Params.CIPPEndpoint $TenantFilter = $Request.Query.tenantFilter + $UseReportDB = $Request.Query.UseReportDB $ForwardingOnly = $Request.Query.ForwardingOnly try { - # Call the report function with proper parameters - $ReportParams = @{ - TenantFilter = $TenantFilter - } - if ($ForwardingOnly -eq 'true') { - $ReportParams.ForwardingOnly = $true + # If UseReportDB is specified, retrieve from report database + if ($UseReportDB -eq 'true') { + $ReportParams = @{ + TenantFilter = $TenantFilter + } + if ($ForwardingOnly -eq 'true') { + $ReportParams.ForwardingOnly = $true + } + + try { + $GraphRequest = Get-CIPPMailboxForwardingReport @ReportParams + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) } - try { - $GraphRequest = Get-CIPPMailboxForwardingReport @ReportParams - $StatusCode = [HttpStatusCode]::OK - } catch { - $StatusCode = [HttpStatusCode]::InternalServerError - $GraphRequest = $_.Exception.Message + # Live query from Exchange Online + $Select = 'UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientTypeDetails,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress' + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-Mailbox' + cmdParams = @{} + Select = $Select } - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Mailbox forwarding report listed for $($TenantFilter)" -sev Debug + $Mailboxes = New-ExoRequest @ExoRequest + + $GraphRequest = foreach ($Mailbox in $Mailboxes) { + $HasExternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.ForwardingSmtpAddress) + $HasInternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.ForwardingAddress) + $HasAnyForwarding = $HasExternalForwarding -or $HasInternalForwarding + + $ForwardingType = if ($HasExternalForwarding -and $HasInternalForwarding) { + 'Both' + } elseif ($HasExternalForwarding) { + 'External' + } elseif ($HasInternalForwarding) { + 'Internal' + } else { + 'None' + } + + $ForwardTo = if ($HasExternalForwarding) { + $Mailbox.ForwardingSmtpAddress -replace 'smtp:', '' + } elseif ($HasInternalForwarding) { + $Mailbox.ForwardingAddress + } else { + $null + } + + [PSCustomObject]@{ + UPN = $Mailbox.UserPrincipalName + DisplayName = $Mailbox.DisplayName + PrimarySmtpAddress = $Mailbox.PrimarySMTPAddress + RecipientTypeDetails = $Mailbox.RecipientTypeDetails + ForwardingType = $ForwardingType + ForwardTo = $ForwardTo + ForwardingSmtpAddress = $Mailbox.ForwardingSmtpAddress -replace 'smtp:', '' + InternalForwardingAddress = $Mailbox.ForwardingAddress + DeliverToMailboxAndForward = $Mailbox.DeliverToMailboxAndForward + HasForwarding = $HasAnyForwarding + } + } - return ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @($GraphRequest) - }) + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Mailbox forwarding listed for $($TenantFilter)" -sev Debug + $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From bf7ff6a6900da08e3b0196418ddd9e10fd0e9d2d Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 10:39:34 +0000 Subject: [PATCH 04/11] Remove ForwardingOnly parameter - always filter to forwarding mailboxes A mailbox forwarding report should only contain mailboxes with forwarding. --- .../Reports/Invoke-ListMailboxForwarding.ps1 | 24 +++++-------- .../Get-CIPPMailboxForwardingReport.ps1 | 34 +++++-------------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 index 836b1ff8d3a5..5042ef96fb0e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 @@ -11,20 +11,12 @@ function Invoke-ListMailboxForwarding { $APIName = $Request.Params.CIPPEndpoint $TenantFilter = $Request.Query.tenantFilter $UseReportDB = $Request.Query.UseReportDB - $ForwardingOnly = $Request.Query.ForwardingOnly try { # If UseReportDB is specified, retrieve from report database if ($UseReportDB -eq 'true') { - $ReportParams = @{ - TenantFilter = $TenantFilter - } - if ($ForwardingOnly -eq 'true') { - $ReportParams.ForwardingOnly = $true - } - try { - $GraphRequest = Get-CIPPMailboxForwardingReport @ReportParams + $GraphRequest = Get-CIPPMailboxForwardingReport -TenantFilter $TenantFilter $StatusCode = [HttpStatusCode]::OK } catch { $StatusCode = [HttpStatusCode]::InternalServerError @@ -53,22 +45,23 @@ function Invoke-ListMailboxForwarding { $HasInternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.ForwardingAddress) $HasAnyForwarding = $HasExternalForwarding -or $HasInternalForwarding + # Only include mailboxes with forwarding configured + if (-not $HasAnyForwarding) { + continue + } + $ForwardingType = if ($HasExternalForwarding -and $HasInternalForwarding) { 'Both' } elseif ($HasExternalForwarding) { 'External' - } elseif ($HasInternalForwarding) { - 'Internal' } else { - 'None' + 'Internal' } $ForwardTo = if ($HasExternalForwarding) { $Mailbox.ForwardingSmtpAddress -replace 'smtp:', '' - } elseif ($HasInternalForwarding) { - $Mailbox.ForwardingAddress } else { - $null + $Mailbox.ForwardingAddress } [PSCustomObject]@{ @@ -81,7 +74,6 @@ function Invoke-ListMailboxForwarding { ForwardingSmtpAddress = $Mailbox.ForwardingSmtpAddress -replace 'smtp:', '' InternalForwardingAddress = $Mailbox.ForwardingAddress DeliverToMailboxAndForward = $Mailbox.DeliverToMailboxAndForward - HasForwarding = $HasAnyForwarding } } diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 index 312a29c81d08..a762bd9c1a7a 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 @@ -4,30 +4,20 @@ function Get-CIPPMailboxForwardingReport { Generates a mailbox forwarding report from the CIPP Reporting database .DESCRIPTION - Retrieves mailbox forwarding settings for a tenant from the cached mailbox data. - Shows mailboxes that have external forwarding, internal forwarding, or both configured. + Retrieves mailboxes that have forwarding configured (external, internal, or both) + from the cached mailbox data. .PARAMETER TenantFilter The tenant to generate the report for - .PARAMETER ForwardingOnly - If specified, only returns mailboxes that have forwarding configured - .EXAMPLE Get-CIPPMailboxForwardingReport -TenantFilter 'contoso.onmicrosoft.com' - Gets all mailboxes with their forwarding settings - - .EXAMPLE - Get-CIPPMailboxForwardingReport -TenantFilter 'contoso.onmicrosoft.com' -ForwardingOnly - Gets only mailboxes that have forwarding configured + Gets all mailboxes with forwarding configured #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter, - - [Parameter(Mandatory = $false)] - [switch]$ForwardingOnly + [string]$TenantFilter ) try { @@ -45,9 +35,8 @@ function Get-CIPPMailboxForwardingReport { $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($Tenant in $Tenants) { try { - $TenantResults = Get-CIPPMailboxForwardingReport -TenantFilter $Tenant -ForwardingOnly:$ForwardingOnly + $TenantResults = Get-CIPPMailboxForwardingReport -TenantFilter $Tenant foreach ($Result in $TenantResults) { - # Add Tenant property to each result $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force $AllResults.Add($Result) } @@ -77,8 +66,8 @@ function Get-CIPPMailboxForwardingReport { $HasInternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.InternalForwardingAddress) $HasAnyForwarding = $HasExternalForwarding -or $HasInternalForwarding - # Skip mailboxes without forwarding if ForwardingOnly is specified - if ($ForwardingOnly -and -not $HasAnyForwarding) { + # Only include mailboxes with forwarding configured + if (-not $HasAnyForwarding) { continue } @@ -87,19 +76,15 @@ function Get-CIPPMailboxForwardingReport { 'Both' } elseif ($HasExternalForwarding) { 'External' - } elseif ($HasInternalForwarding) { - 'Internal' } else { - 'None' + 'Internal' } # Build the forward-to address display $ForwardTo = if ($HasExternalForwarding) { $Mailbox.ForwardingSmtpAddress - } elseif ($HasInternalForwarding) { - $Mailbox.InternalForwardingAddress } else { - $null + $Mailbox.InternalForwardingAddress } $Report.Add([PSCustomObject]@{ @@ -112,7 +97,6 @@ function Get-CIPPMailboxForwardingReport { ForwardingSmtpAddress = $Mailbox.ForwardingSmtpAddress InternalForwardingAddress = $Mailbox.InternalForwardingAddress DeliverToMailboxAndForward = $Mailbox.DeliverToMailboxAndForward - HasForwarding = $HasAnyForwarding Tenant = $TenantFilter CacheTimestamp = $CacheTimestamp }) From b4da8ed274ad539be81b9fefcb3147ece074209d Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 10:56:42 +0000 Subject: [PATCH 05/11] Show both addresses in ForwardTo when both internal and external are configured --- .../Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 | 4 +++- Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 index 5042ef96fb0e..cade4e0bf1a9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 @@ -58,7 +58,9 @@ function Invoke-ListMailboxForwarding { 'Internal' } - $ForwardTo = if ($HasExternalForwarding) { + $ForwardTo = if ($HasExternalForwarding -and $HasInternalForwarding) { + "$($Mailbox.ForwardingSmtpAddress -replace 'smtp:', ''), $($Mailbox.ForwardingAddress)" + } elseif ($HasExternalForwarding) { $Mailbox.ForwardingSmtpAddress -replace 'smtp:', '' } else { $Mailbox.ForwardingAddress diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 index a762bd9c1a7a..2baa5462dff1 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 @@ -81,7 +81,9 @@ function Get-CIPPMailboxForwardingReport { } # Build the forward-to address display - $ForwardTo = if ($HasExternalForwarding) { + $ForwardTo = if ($HasExternalForwarding -and $HasInternalForwarding) { + "$($Mailbox.ForwardingSmtpAddress), $($Mailbox.InternalForwardingAddress)" + } elseif ($HasExternalForwarding) { $Mailbox.ForwardingSmtpAddress } else { $Mailbox.InternalForwardingAddress From 7978dd83989ce69e31522ad6d67f3fe4853cdb82 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 11:02:26 +0000 Subject: [PATCH 06/11] ForwardTo shows only external when both configured (external takes precedence) --- .../Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 | 5 ++--- Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 index cade4e0bf1a9..f28b6a32509b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 @@ -58,9 +58,8 @@ function Invoke-ListMailboxForwarding { 'Internal' } - $ForwardTo = if ($HasExternalForwarding -and $HasInternalForwarding) { - "$($Mailbox.ForwardingSmtpAddress -replace 'smtp:', ''), $($Mailbox.ForwardingAddress)" - } elseif ($HasExternalForwarding) { + # External takes precedence when both are configured + $ForwardTo = if ($HasExternalForwarding) { $Mailbox.ForwardingSmtpAddress -replace 'smtp:', '' } else { $Mailbox.ForwardingAddress diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 index 2baa5462dff1..a09c30d75c4f 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 @@ -80,10 +80,8 @@ function Get-CIPPMailboxForwardingReport { 'Internal' } - # Build the forward-to address display - $ForwardTo = if ($HasExternalForwarding -and $HasInternalForwarding) { - "$($Mailbox.ForwardingSmtpAddress), $($Mailbox.InternalForwardingAddress)" - } elseif ($HasExternalForwarding) { + # Build the forward-to address display (external takes precedence) + $ForwardTo = if ($HasExternalForwarding) { $Mailbox.ForwardingSmtpAddress } else { $Mailbox.InternalForwardingAddress From a80f59e358219a0b3f1955ddca5b3c5a527a9e8b Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Mar 2026 13:12:10 +0000 Subject: [PATCH 07/11] Remove Both forwarding type - external takes precedence --- .../Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 | 5 ++--- Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 index f28b6a32509b..2eae2f67dd97 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxForwarding.ps1 @@ -50,9 +50,8 @@ function Invoke-ListMailboxForwarding { continue } - $ForwardingType = if ($HasExternalForwarding -and $HasInternalForwarding) { - 'Both' - } elseif ($HasExternalForwarding) { + # External takes precedence when both are configured + $ForwardingType = if ($HasExternalForwarding) { 'External' } else { 'Internal' diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 index a09c30d75c4f..ccc3dad5ac59 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1 @@ -71,10 +71,8 @@ function Get-CIPPMailboxForwardingReport { continue } - # Determine forwarding type for display - $ForwardingType = if ($HasExternalForwarding -and $HasInternalForwarding) { - 'Both' - } elseif ($HasExternalForwarding) { + # Determine forwarding type for display (external takes precedence) + $ForwardingType = if ($HasExternalForwarding) { 'External' } else { 'Internal' From 8ced808d0512db0baf413dd701e71a312ca862aa Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Mar 2026 10:02:39 -0400 Subject: [PATCH 08/11] fix: try parsing booleans instead of assuming it is one --- .../Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 index e3f5ed469340..b0cf533434aa 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 @@ -25,7 +25,8 @@ function Set-CIPPOffloadFunctionTriggers { # Get offloading state from Config table $Table = Get-CippTable -tablename 'Config' $OffloadConfig = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'OffloadFunctions' and RowKey eq 'OffloadFunctions'" - $OffloadEnabled = [bool]$OffloadConfig.state + $OffloadEnabled = $false + [bool]::TryParse($OffloadConfig.state, [ref]$OffloadEnabled) | Out-Null # Determine resource group if ($env:WEBSITE_RESOURCE_GROUP) { From 720017cd694ad7a55dde9a6b162760e5f046ab58 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Mar 2026 11:25:26 -0400 Subject: [PATCH 09/11] fix: track last setting update for triggers also add env var backup table --- .../GraphHelper/Set-CIPPEnvVarBackup.ps1 | 68 +++++++++++++++++++ .../Set-CIPPOffloadFunctionTriggers.ps1 | 21 ++++++ profile.ps1 | 1 + 3 files changed, 90 insertions(+) create mode 100644 Modules/CIPPCore/Public/GraphHelper/Set-CIPPEnvVarBackup.ps1 diff --git a/Modules/CIPPCore/Public/GraphHelper/Set-CIPPEnvVarBackup.ps1 b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPEnvVarBackup.ps1 new file mode 100644 index 000000000000..fca4657d58f6 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPEnvVarBackup.ps1 @@ -0,0 +1,68 @@ +function Set-CIPPEnvVarBackup { + param() + + $FunctionAppName = $env:WEBSITE_SITE_NAME + $PropertiesToBackup = @( + 'AzureWebJobsStorage' + 'WEBSITE_RUN_FROM_PACKAGE' + 'FUNCTIONS_EXTENSION_VERSION' + 'FUNCTIONS_WORKER_RUNTIME' + 'CIPP_HOSTED' + 'CIPP_HOSTED_KV_SUB' + 'WEBSITE_ENABLE_SYNC_UPDATE_SITE' + 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS' + ) + + $RequiredProperties = @('AzureWebJobsStorage', 'FUNCTIONS_EXTENSION_VERSION', 'FUNCTIONS_WORKER_RUNTIME', 'WEBSITE_RUN_FROM_PACKAGE') + + if ($env:WEBSITE_SKU -eq 'FlexConsumption') { + $RequiredProperties = $RequiredProperties | Where-Object { $_ -ne 'WEBSITE_RUN_FROM_PACKAGE' } + } + + $Backup = @{} + foreach ($Property in $PropertiesToBackup) { + $Backup[$Property] = [environment]::GetEnvironmentVariable($Property) + } + + $EnvBackupTable = Get-CIPPTable -tablename 'EnvVarBackups' + $CurrentBackup = Get-CIPPAzDataTableEntity @EnvBackupTable -Filter "PartitionKey eq 'EnvVarBackup' and RowKey eq '$FunctionAppName'" + + # ConvertFrom-Json returns PSCustomObject - convert to hashtable for consistent key/value access + $CurrentValues = @{} + if ($CurrentBackup -and $CurrentBackup.Values) { + ($CurrentBackup.Values | ConvertFrom-Json).PSObject.Properties | ForEach-Object { + $CurrentValues[$_.Name] = $_.Value + } + } + + $IsNew = $CurrentValues.Count -eq 0 + + if ($IsNew) { + # First capture - write everything from the live environment + $SavedValues = $Backup + Write-Information "Creating new environment variable backup for $FunctionAppName" + } else { + # Backup already exists - keep existing values fixed, only backfill any properties not yet captured + $SavedValues = $CurrentValues + foreach ($Property in $PropertiesToBackup) { + if (-not $SavedValues[$Property] -and $Backup[$Property]) { + Write-Information "Backfilling missing backup property '$Property' from current environment." + $SavedValues[$Property] = $Backup[$Property] + } + } + Write-Information "Environment variable backup already exists for $FunctionAppName - preserving fixed values" + } + + # Validate all required properties are present in the final backup + $MissingRequired = $RequiredProperties | Where-Object { -not $SavedValues[$_] } + if ($MissingRequired) { + Write-Warning "Environment variable backup for $FunctionAppName is missing required properties: $($MissingRequired -join ', ')" + } + + $Entity = @{ + PartitionKey = 'EnvVarBackup' + RowKey = $FunctionAppName + Values = [string]($SavedValues | ConvertTo-Json -Compress) + } + Add-CIPPAzDataTableEntity @EnvBackupTable -Entity $Entity -Force | Out-Null +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 index b0cf533434aa..48d155353998 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 @@ -28,6 +28,15 @@ function Set-CIPPOffloadFunctionTriggers { $OffloadEnabled = $false [bool]::TryParse($OffloadConfig.state, [ref]$OffloadEnabled) | Out-Null + # Trigger Last change table + $TriggerChangeTable = Get-CippTable -tablename 'OffloadTriggerChange' + $LastChange = Get-CIPPAzDataTableEntity @TriggerChangeTable + + if ($LastChange -and $LastChange.Timestamp -gt (Get-Date).AddMinutes(-30).ToUniversalTime() -and $LastChange.Offloading -eq $OffloadEnabled) { + Write-Information "Last trigger change was at $LastChange, skipping update to avoid rapid changes." + return $true + } + # Determine resource group if ($env:WEBSITE_RESOURCE_GROUP) { $ResourceGroupName = $env:WEBSITE_RESOURCE_GROUP @@ -70,6 +79,12 @@ function Set-CIPPOffloadFunctionTriggers { # Update app settings only if there are changes to make if ($AppSettings.Count -gt 0) { if ($PSCmdlet.ShouldProcess($FunctionAppName, 'Disable non-HTTP triggers')) { + $LastChange = @{ + PartitionKey = 'TriggerChange' + RowKey = 'LastChange' + Offloading = $OffloadEnabled + } + Add-CIPPAzDataTableEntity @TriggerChangeTable -Entity $LastChange -Force | Out-Null Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $ResourceGroupName -AppSetting $AppSettings | Out-Null Write-Information "Successfully disabled $($AppSettings.Count) non-HTTP trigger(s) on $FunctionAppName" } @@ -95,6 +110,12 @@ function Set-CIPPOffloadFunctionTriggers { # Update app settings with removal of keys only if there are changes to make if ($RemoveKeys.Count -gt 0) { if ($PSCmdlet.ShouldProcess($FunctionAppName, 'Re-enable non-HTTP triggers')) { + $LastChange = @{ + PartitionKey = 'TriggerChange' + RowKey = 'LastChange' + Offloading = $OffloadEnabled + } + Add-CIPPAzDataTableEntity @TriggerChangeTable -Entity $LastChange -Force | Out-Null Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $ResourceGroupName -AppSetting @{} -RemoveKeys $RemoveKeys | Out-Null Write-Information "Successfully re-enabled $($RemoveKeys.Count) non-HTTP trigger(s) on $FunctionAppName" } diff --git a/profile.ps1 b/profile.ps1 index 9d52861225d7..cf882819db24 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -127,6 +127,7 @@ if (!$LastStartup -or $CurrentVersion -ne $LastStartup.Version) { $SwVersion.Stop() $Timings['VersionCheck'] = $SwVersion.Elapsed.TotalMilliseconds +Set-CIPPEnvVarBackup if ($env:AzureWebJobsStorage -ne 'UseDevelopmentStorage=true' -and $env:NonLocalHostAzurite -ne 'true') { Set-CIPPOffloadFunctionTriggers } From f41831239c61a96702df5aa12f58166e12f2b8ef Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Mar 2026 11:31:14 -0400 Subject: [PATCH 10/11] chore: bump version to 10.2.1 --- host.json | 4 ++-- version_latest.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/host.json b/host.json index 1246bb822fed..32862eb0e52d 100644 --- a/host.json +++ b/host.json @@ -16,9 +16,9 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.2.0", + "defaultVersion": "10.2.1", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } } -} +} \ No newline at end of file diff --git a/version_latest.txt b/version_latest.txt index 2bd6f7e39277..85651cb5eaa6 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.2.0 +10.2.1 \ No newline at end of file From d40106398ff900c7285489ed990fe304b8c457e8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Mar 2026 11:36:42 -0400 Subject: [PATCH 11/11] fix: add backfill setting --- .../GraphHelper/Update-CIPPAzFunctionAppSetting.ps1 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Modules/CIPPCore/Public/GraphHelper/Update-CIPPAzFunctionAppSetting.ps1 b/Modules/CIPPCore/Public/GraphHelper/Update-CIPPAzFunctionAppSetting.ps1 index e39c07e9e6cc..878fdfb5fab0 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Update-CIPPAzFunctionAppSetting.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Update-CIPPAzFunctionAppSetting.ps1 @@ -62,6 +62,19 @@ function Update-CIPPAzFunctionAppSetting { $currentProps[$prop.Name] = [string]$prop.Value } } + } else { + # Could not retrieve current settings - backfill from EnvVarBackup to avoid overwriting required properties with empty values + Write-Warning "Could not retrieve current Function App settings for $Name - attempting to backfill from environment variable backup." + $EnvBackupTable = Get-CIPPTable -tablename 'EnvVarBackups' + $BackupEntity = Get-CIPPAzDataTableEntity @EnvBackupTable -Filter "PartitionKey eq 'EnvVarBackup' and RowKey eq '$Name'" + if ($BackupEntity -and $BackupEntity.Values) { + ($BackupEntity.Values | ConvertFrom-Json).PSObject.Properties | ForEach-Object { + if ($_.Value) { $currentProps[$_.Name] = [string]$_.Value } + } + Write-Information "Backfilled $($currentProps.Count) properties from environment variable backup for $Name" + } else { + throw "Failed to retrieve current settings for Function App $Name and no backup found - aborting update to avoid potential misconfiguration." + } } # Merge requested settings