Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
function Invoke-ListMailboxForwarding {
<#
.FUNCTIONALITY
Entrypoint
.ROLE
Exchange.Mailbox.Read
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)

$APIName = $Request.Params.CIPPEndpoint
$TenantFilter = $Request.Query.tenantFilter
$UseReportDB = $Request.Query.UseReportDB

try {
# If UseReportDB is specified, retrieve from report database
if ($UseReportDB -eq 'true') {
try {
$GraphRequest = Get-CIPPMailboxForwardingReport -TenantFilter $TenantFilter
$StatusCode = [HttpStatusCode]::OK
} catch {
$StatusCode = [HttpStatusCode]::InternalServerError
$GraphRequest = $_.Exception.Message
}

return ([HttpResponseContext]@{
StatusCode = $StatusCode
Body = @($GraphRequest)
})
}

# Live query from Exchange Online
$Select = 'UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientTypeDetails,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress'
$ExoRequest = @{
tenantid = $TenantFilter
cmdlet = 'Get-Mailbox'
cmdParams = @{}
Select = $Select
}

$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

# Only include mailboxes with forwarding configured
if (-not $HasAnyForwarding) {
continue
}

# External takes precedence when both are configured
$ForwardingType = if ($HasExternalForwarding) {
'External'
} else {
'Internal'
}

# External takes precedence when both are configured
$ForwardTo = if ($HasExternalForwarding) {
$Mailbox.ForwardingSmtpAddress -replace 'smtp:', ''
} else {
$Mailbox.ForwardingAddress
}

[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
}
}

Write-LogMessage -API $APIName -tenant $TenantFilter -message "Mailbox forwarding listed for $($TenantFilter)" -sev Debug
$StatusCode = [HttpStatusCode]::OK

} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
$StatusCode = [HttpStatusCode]::Forbidden
$GraphRequest = $ErrorMessage
}

return ([HttpResponseContext]@{
StatusCode = $StatusCode
Body = @($GraphRequest)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
113 changes: 113 additions & 0 deletions Modules/CIPPCore/Public/Get-CIPPMailboxForwardingReport.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
function Get-CIPPMailboxForwardingReport {
<#
.SYNOPSIS
Generates a mailbox forwarding report from the CIPP Reporting database

.DESCRIPTION
Retrieves mailboxes that have forwarding configured (external, internal, or both)
from the cached mailbox data.

.PARAMETER TenantFilter
The tenant to generate the report for

.EXAMPLE
Get-CIPPMailboxForwardingReport -TenantFilter 'contoso.onmicrosoft.com'
Gets all mailboxes with forwarding configured
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantFilter
)

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
foreach ($Result in $TenantResults) {
$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

# Only include mailboxes with forwarding configured
if (-not $HasAnyForwarding) {
continue
}

# Determine forwarding type for display (external takes precedence)
$ForwardingType = if ($HasExternalForwarding) {
'External'
} else {
'Internal'
}

# Build the forward-to address display (external takes precedence)
$ForwardTo = if ($HasExternalForwarding) {
$Mailbox.ForwardingSmtpAddress
} else {
$Mailbox.InternalForwardingAddress
}

$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
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)"
}
}
68 changes: 68 additions & 0 deletions Modules/CIPPCore/Public/GraphHelper/Set-CIPPEnvVarBackup.ps1
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ 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

# 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) {
Expand Down Expand Up @@ -69,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"
}
Expand All @@ -94,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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion Modules/CIPPCore/Public/New-CippUser.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading