Skip to content

Commit 0beffe8

Browse files
authored
Merge pull request #112 from KelvinTegelaar/master
[pull] master from KelvinTegelaar:master
2 parents df812a7 + 81a63ce commit 0beffe8

File tree

10 files changed

+338
-6
lines changed

10 files changed

+338
-6
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
function Invoke-ListMailboxForwarding {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
.ROLE
6+
Exchange.Mailbox.Read
7+
#>
8+
[CmdletBinding()]
9+
param($Request, $TriggerMetadata)
10+
11+
$APIName = $Request.Params.CIPPEndpoint
12+
$TenantFilter = $Request.Query.tenantFilter
13+
$UseReportDB = $Request.Query.UseReportDB
14+
15+
try {
16+
# If UseReportDB is specified, retrieve from report database
17+
if ($UseReportDB -eq 'true') {
18+
try {
19+
$GraphRequest = Get-CIPPMailboxForwardingReport -TenantFilter $TenantFilter
20+
$StatusCode = [HttpStatusCode]::OK
21+
} catch {
22+
$StatusCode = [HttpStatusCode]::InternalServerError
23+
$GraphRequest = $_.Exception.Message
24+
}
25+
26+
return ([HttpResponseContext]@{
27+
StatusCode = $StatusCode
28+
Body = @($GraphRequest)
29+
})
30+
}
31+
32+
# Live query from Exchange Online
33+
$Select = 'UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientTypeDetails,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress'
34+
$ExoRequest = @{
35+
tenantid = $TenantFilter
36+
cmdlet = 'Get-Mailbox'
37+
cmdParams = @{}
38+
Select = $Select
39+
}
40+
41+
$Mailboxes = New-ExoRequest @ExoRequest
42+
43+
$GraphRequest = foreach ($Mailbox in $Mailboxes) {
44+
$HasExternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.ForwardingSmtpAddress)
45+
$HasInternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.ForwardingAddress)
46+
$HasAnyForwarding = $HasExternalForwarding -or $HasInternalForwarding
47+
48+
# Only include mailboxes with forwarding configured
49+
if (-not $HasAnyForwarding) {
50+
continue
51+
}
52+
53+
# External takes precedence when both are configured
54+
$ForwardingType = if ($HasExternalForwarding) {
55+
'External'
56+
} else {
57+
'Internal'
58+
}
59+
60+
# External takes precedence when both are configured
61+
$ForwardTo = if ($HasExternalForwarding) {
62+
$Mailbox.ForwardingSmtpAddress -replace 'smtp:', ''
63+
} else {
64+
$Mailbox.ForwardingAddress
65+
}
66+
67+
[PSCustomObject]@{
68+
UPN = $Mailbox.UserPrincipalName
69+
DisplayName = $Mailbox.DisplayName
70+
PrimarySmtpAddress = $Mailbox.PrimarySMTPAddress
71+
RecipientTypeDetails = $Mailbox.RecipientTypeDetails
72+
ForwardingType = $ForwardingType
73+
ForwardTo = $ForwardTo
74+
ForwardingSmtpAddress = $Mailbox.ForwardingSmtpAddress -replace 'smtp:', ''
75+
InternalForwardingAddress = $Mailbox.ForwardingAddress
76+
DeliverToMailboxAndForward = $Mailbox.DeliverToMailboxAndForward
77+
}
78+
}
79+
80+
Write-LogMessage -API $APIName -tenant $TenantFilter -message "Mailbox forwarding listed for $($TenantFilter)" -sev Debug
81+
$StatusCode = [HttpStatusCode]::OK
82+
83+
} catch {
84+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
85+
$StatusCode = [HttpStatusCode]::Forbidden
86+
$GraphRequest = $ErrorMessage
87+
}
88+
89+
return ([HttpResponseContext]@{
90+
StatusCode = $StatusCode
91+
Body = @($GraphRequest)
92+
})
93+
}

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ function Invoke-EditUser {
3232
try {
3333
Write-Host "$([boolean]$UserObj.MustChangePass)"
3434
$UserPrincipalName = "$($UserObj.username)@$($UserObj.Domain ? $UserObj.Domain : $UserObj.primDomain.value)"
35+
$normalizedOtherMails = @(
36+
@($UserObj.otherMails) | ForEach-Object {
37+
if ($null -ne $_) {
38+
[string]$_ -split ','
39+
}
40+
} | ForEach-Object {
41+
$_.Trim()
42+
} | Where-Object {
43+
-not [string]::IsNullOrWhiteSpace($_)
44+
}
45+
)
3546
$BodyToship = [pscustomobject] @{
3647
'givenName' = $UserObj.givenName
3748
'surname' = $UserObj.surname
@@ -49,7 +60,7 @@ function Invoke-EditUser {
4960
'country' = $UserObj.country
5061
'companyName' = $UserObj.companyName
5162
'businessPhones' = $UserObj.businessPhones ? @($UserObj.businessPhones) : @()
52-
'otherMails' = $UserObj.otherMails ? @($UserObj.otherMails) : @()
63+
'otherMails' = $normalizedOtherMails
5364
'passwordProfile' = @{
5465
'forceChangePasswordNextSignIn' = [bool]$UserObj.MustChangePass
5566
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
function Get-CIPPMailboxForwardingReport {
2+
<#
3+
.SYNOPSIS
4+
Generates a mailbox forwarding report from the CIPP Reporting database
5+
6+
.DESCRIPTION
7+
Retrieves mailboxes that have forwarding configured (external, internal, or both)
8+
from the cached mailbox data.
9+
10+
.PARAMETER TenantFilter
11+
The tenant to generate the report for
12+
13+
.EXAMPLE
14+
Get-CIPPMailboxForwardingReport -TenantFilter 'contoso.onmicrosoft.com'
15+
Gets all mailboxes with forwarding configured
16+
#>
17+
[CmdletBinding()]
18+
param(
19+
[Parameter(Mandatory = $true)]
20+
[string]$TenantFilter
21+
)
22+
23+
try {
24+
Write-LogMessage -API 'MailboxForwardingReport' -tenant $TenantFilter -message 'Generating mailbox forwarding report' -sev Debug
25+
26+
# Handle AllTenants
27+
if ($TenantFilter -eq 'AllTenants') {
28+
# Get all tenants that have mailbox data
29+
$AllMailboxItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Mailboxes'
30+
$Tenants = @($AllMailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } | Select-Object -ExpandProperty PartitionKey -Unique)
31+
32+
$TenantList = Get-Tenants -IncludeErrors
33+
$Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ }
34+
35+
$AllResults = [System.Collections.Generic.List[PSCustomObject]]::new()
36+
foreach ($Tenant in $Tenants) {
37+
try {
38+
$TenantResults = Get-CIPPMailboxForwardingReport -TenantFilter $Tenant
39+
foreach ($Result in $TenantResults) {
40+
$Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force
41+
$AllResults.Add($Result)
42+
}
43+
} catch {
44+
Write-LogMessage -API 'MailboxForwardingReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning
45+
}
46+
}
47+
return $AllResults
48+
}
49+
50+
# Get mailboxes from reporting DB
51+
$MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' }
52+
if (-not $MailboxItems) {
53+
throw 'No mailbox data found in reporting database. Sync the mailbox data first.'
54+
}
55+
56+
# Get the most recent cache timestamp
57+
$CacheTimestamp = ($MailboxItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp
58+
59+
# Parse mailbox data and build report
60+
$Report = [System.Collections.Generic.List[PSCustomObject]]::new()
61+
foreach ($Item in $MailboxItems) {
62+
$Mailbox = $Item.Data | ConvertFrom-Json
63+
64+
# Determine forwarding status
65+
$HasExternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.ForwardingSmtpAddress)
66+
$HasInternalForwarding = -not [string]::IsNullOrWhiteSpace($Mailbox.InternalForwardingAddress)
67+
$HasAnyForwarding = $HasExternalForwarding -or $HasInternalForwarding
68+
69+
# Only include mailboxes with forwarding configured
70+
if (-not $HasAnyForwarding) {
71+
continue
72+
}
73+
74+
# Determine forwarding type for display (external takes precedence)
75+
$ForwardingType = if ($HasExternalForwarding) {
76+
'External'
77+
} else {
78+
'Internal'
79+
}
80+
81+
# Build the forward-to address display (external takes precedence)
82+
$ForwardTo = if ($HasExternalForwarding) {
83+
$Mailbox.ForwardingSmtpAddress
84+
} else {
85+
$Mailbox.InternalForwardingAddress
86+
}
87+
88+
$Report.Add([PSCustomObject]@{
89+
UPN = $Mailbox.UPN
90+
DisplayName = $Mailbox.displayName
91+
PrimarySmtpAddress = $Mailbox.primarySmtpAddress
92+
RecipientTypeDetails = $Mailbox.recipientTypeDetails
93+
ForwardingType = $ForwardingType
94+
ForwardTo = $ForwardTo
95+
ForwardingSmtpAddress = $Mailbox.ForwardingSmtpAddress
96+
InternalForwardingAddress = $Mailbox.InternalForwardingAddress
97+
DeliverToMailboxAndForward = $Mailbox.DeliverToMailboxAndForward
98+
Tenant = $TenantFilter
99+
CacheTimestamp = $CacheTimestamp
100+
})
101+
}
102+
103+
# Sort by display name
104+
$Report = $Report | Sort-Object -Property DisplayName
105+
106+
Write-LogMessage -API 'MailboxForwardingReport' -tenant $TenantFilter -message "Generated forwarding report with $($Report.Count) entries" -sev Debug
107+
return $Report
108+
109+
} catch {
110+
Write-LogMessage -API 'MailboxForwardingReport' -tenant $TenantFilter -message "Failed to generate mailbox forwarding report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_)
111+
throw "Failed to generate mailbox forwarding report: $($_.Exception.Message)"
112+
}
113+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
function Set-CIPPEnvVarBackup {
2+
param()
3+
4+
$FunctionAppName = $env:WEBSITE_SITE_NAME
5+
$PropertiesToBackup = @(
6+
'AzureWebJobsStorage'
7+
'WEBSITE_RUN_FROM_PACKAGE'
8+
'FUNCTIONS_EXTENSION_VERSION'
9+
'FUNCTIONS_WORKER_RUNTIME'
10+
'CIPP_HOSTED'
11+
'CIPP_HOSTED_KV_SUB'
12+
'WEBSITE_ENABLE_SYNC_UPDATE_SITE'
13+
'WEBSITE_AUTH_AAD_ALLOWED_TENANTS'
14+
)
15+
16+
$RequiredProperties = @('AzureWebJobsStorage', 'FUNCTIONS_EXTENSION_VERSION', 'FUNCTIONS_WORKER_RUNTIME', 'WEBSITE_RUN_FROM_PACKAGE')
17+
18+
if ($env:WEBSITE_SKU -eq 'FlexConsumption') {
19+
$RequiredProperties = $RequiredProperties | Where-Object { $_ -ne 'WEBSITE_RUN_FROM_PACKAGE' }
20+
}
21+
22+
$Backup = @{}
23+
foreach ($Property in $PropertiesToBackup) {
24+
$Backup[$Property] = [environment]::GetEnvironmentVariable($Property)
25+
}
26+
27+
$EnvBackupTable = Get-CIPPTable -tablename 'EnvVarBackups'
28+
$CurrentBackup = Get-CIPPAzDataTableEntity @EnvBackupTable -Filter "PartitionKey eq 'EnvVarBackup' and RowKey eq '$FunctionAppName'"
29+
30+
# ConvertFrom-Json returns PSCustomObject - convert to hashtable for consistent key/value access
31+
$CurrentValues = @{}
32+
if ($CurrentBackup -and $CurrentBackup.Values) {
33+
($CurrentBackup.Values | ConvertFrom-Json).PSObject.Properties | ForEach-Object {
34+
$CurrentValues[$_.Name] = $_.Value
35+
}
36+
}
37+
38+
$IsNew = $CurrentValues.Count -eq 0
39+
40+
if ($IsNew) {
41+
# First capture - write everything from the live environment
42+
$SavedValues = $Backup
43+
Write-Information "Creating new environment variable backup for $FunctionAppName"
44+
} else {
45+
# Backup already exists - keep existing values fixed, only backfill any properties not yet captured
46+
$SavedValues = $CurrentValues
47+
foreach ($Property in $PropertiesToBackup) {
48+
if (-not $SavedValues[$Property] -and $Backup[$Property]) {
49+
Write-Information "Backfilling missing backup property '$Property' from current environment."
50+
$SavedValues[$Property] = $Backup[$Property]
51+
}
52+
}
53+
Write-Information "Environment variable backup already exists for $FunctionAppName - preserving fixed values"
54+
}
55+
56+
# Validate all required properties are present in the final backup
57+
$MissingRequired = $RequiredProperties | Where-Object { -not $SavedValues[$_] }
58+
if ($MissingRequired) {
59+
Write-Warning "Environment variable backup for $FunctionAppName is missing required properties: $($MissingRequired -join ', ')"
60+
}
61+
62+
$Entity = @{
63+
PartitionKey = 'EnvVarBackup'
64+
RowKey = $FunctionAppName
65+
Values = [string]($SavedValues | ConvertTo-Json -Compress)
66+
}
67+
Add-CIPPAzDataTableEntity @EnvBackupTable -Entity $Entity -Force | Out-Null
68+
}

Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,17 @@ function Set-CIPPOffloadFunctionTriggers {
2525
# Get offloading state from Config table
2626
$Table = Get-CippTable -tablename 'Config'
2727
$OffloadConfig = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'OffloadFunctions' and RowKey eq 'OffloadFunctions'"
28-
$OffloadEnabled = [bool]$OffloadConfig.state
28+
$OffloadEnabled = $false
29+
[bool]::TryParse($OffloadConfig.state, [ref]$OffloadEnabled) | Out-Null
30+
31+
# Trigger Last change table
32+
$TriggerChangeTable = Get-CippTable -tablename 'OffloadTriggerChange'
33+
$LastChange = Get-CIPPAzDataTableEntity @TriggerChangeTable
34+
35+
if ($LastChange -and $LastChange.Timestamp -gt (Get-Date).AddMinutes(-30).ToUniversalTime() -and $LastChange.Offloading -eq $OffloadEnabled) {
36+
Write-Information "Last trigger change was at $LastChange, skipping update to avoid rapid changes."
37+
return $true
38+
}
2939

3040
# Determine resource group
3141
if ($env:WEBSITE_RESOURCE_GROUP) {
@@ -69,6 +79,12 @@ function Set-CIPPOffloadFunctionTriggers {
6979
# Update app settings only if there are changes to make
7080
if ($AppSettings.Count -gt 0) {
7181
if ($PSCmdlet.ShouldProcess($FunctionAppName, 'Disable non-HTTP triggers')) {
82+
$LastChange = @{
83+
PartitionKey = 'TriggerChange'
84+
RowKey = 'LastChange'
85+
Offloading = $OffloadEnabled
86+
}
87+
Add-CIPPAzDataTableEntity @TriggerChangeTable -Entity $LastChange -Force | Out-Null
7288
Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $ResourceGroupName -AppSetting $AppSettings | Out-Null
7389
Write-Information "Successfully disabled $($AppSettings.Count) non-HTTP trigger(s) on $FunctionAppName"
7490
}
@@ -94,6 +110,12 @@ function Set-CIPPOffloadFunctionTriggers {
94110
# Update app settings with removal of keys only if there are changes to make
95111
if ($RemoveKeys.Count -gt 0) {
96112
if ($PSCmdlet.ShouldProcess($FunctionAppName, 'Re-enable non-HTTP triggers')) {
113+
$LastChange = @{
114+
PartitionKey = 'TriggerChange'
115+
RowKey = 'LastChange'
116+
Offloading = $OffloadEnabled
117+
}
118+
Add-CIPPAzDataTableEntity @TriggerChangeTable -Entity $LastChange -Force | Out-Null
97119
Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $ResourceGroupName -AppSetting @{} -RemoveKeys $RemoveKeys | Out-Null
98120
Write-Information "Successfully re-enabled $($RemoveKeys.Count) non-HTTP trigger(s) on $FunctionAppName"
99121
}

Modules/CIPPCore/Public/GraphHelper/Update-CIPPAzFunctionAppSetting.ps1

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ function Update-CIPPAzFunctionAppSetting {
6262
$currentProps[$prop.Name] = [string]$prop.Value
6363
}
6464
}
65+
} else {
66+
# Could not retrieve current settings - backfill from EnvVarBackup to avoid overwriting required properties with empty values
67+
Write-Warning "Could not retrieve current Function App settings for $Name - attempting to backfill from environment variable backup."
68+
$EnvBackupTable = Get-CIPPTable -tablename 'EnvVarBackups'
69+
$BackupEntity = Get-CIPPAzDataTableEntity @EnvBackupTable -Filter "PartitionKey eq 'EnvVarBackup' and RowKey eq '$Name'"
70+
if ($BackupEntity -and $BackupEntity.Values) {
71+
($BackupEntity.Values | ConvertFrom-Json).PSObject.Properties | ForEach-Object {
72+
if ($_.Value) { $currentProps[$_.Name] = [string]$_.Value }
73+
}
74+
Write-Information "Backfilled $($currentProps.Count) properties from environment variable backup for $Name"
75+
} else {
76+
throw "Failed to retrieve current settings for Function App $Name and no backup found - aborting update to avoid potential misconfiguration."
77+
}
6578
}
6679

6780
# Merge requested settings

Modules/CIPPCore/Public/New-CippUser.ps1

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ function New-CIPPUser {
1616
$UserPrincipalName = "$($UserObj.username)@$($UserObj.Domain ? $UserObj.Domain : $UserObj.PrimDomain.value)"
1717
Write-Host "Creating user $UserPrincipalName"
1818
Write-Host "tenant filter is $($UserObj.tenantFilter)"
19+
$normalizedOtherMails = @(
20+
@($UserObj.otherMails) | ForEach-Object {
21+
if ($null -ne $_) {
22+
[string]$_ -split ','
23+
}
24+
} | ForEach-Object {
25+
$_.Trim()
26+
} | Where-Object {
27+
-not [string]::IsNullOrWhiteSpace($_)
28+
}
29+
)
1930
$BodyToship = [pscustomobject] @{
2031
'givenName' = $UserObj.givenName
2132
'surname' = $UserObj.surname
@@ -25,7 +36,7 @@ function New-CIPPUser {
2536
'mailNickname' = $UserObj.username ? $UserObj.username : $UserObj.mailNickname
2637
'userPrincipalName' = $UserPrincipalName
2738
'usageLocation' = $UserObj.usageLocation.value ? $UserObj.usageLocation.value : $UserObj.usageLocation
28-
'otherMails' = $UserObj.otherMails ? @($UserObj.otherMails) : @()
39+
'otherMails' = $normalizedOtherMails
2940
'jobTitle' = $UserObj.jobTitle
3041
'mobilePhone' = $UserObj.mobilePhone
3142
'streetAddress' = $UserObj.streetAddress

0 commit comments

Comments
 (0)