All scripts
Automation 467

Bulk Grant Shared Mailbox Permissions From a CSV File

Reads a CSV of shared mailbox and user pairs and grants Full Access and Send As permissions in Exchange Online, so a batch of access requests does not mean running the same two commands over and over.

Grant-SharedMailboxPermissionFromCsv.ps1
<#
.SYNOPSIS
Grants Full Access and Send As permissions on shared mailboxes in bulk from a CSV file.

.DESCRIPTION
Reads a CSV file listing shared mailbox addresses and the users who need access, then
applies Full Access and Send As permissions in Exchange Online for each row. Useful when
onboarding a batch of users to shared mailboxes at once instead of running Add-MailboxPermission
and Add-RecipientPermission one pair at a time. Skips a row and logs a warning if the mailbox
or user cannot be found, so one bad row does not stop the rest of the batch.

.PARAMETER CsvPath
Path to a CSV file with columns: SharedMailbox, User

.PARAMETER AutoMapping
If set, leaves Outlook auto-mapping on (the default). Pass -AutoMapping:$false to grant
access without the mailbox automatically showing up in the user's Outlook profile.

.EXAMPLE
.\Grant-SharedMailboxPermissionFromCsv.ps1 -CsvPath .\mailbox-access.csv

.EXAMPLE
.\Grant-SharedMailboxPermissionFromCsv.ps1 -CsvPath .\mailbox-access.csv -AutoMapping:$false

.AUTHOR
Shehryar Hassan
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$CsvPath,

    [Parameter(Mandatory = $false)]
    [bool]$AutoMapping = $true
)

if (-not (Get-Command Get-Mailbox -ErrorAction SilentlyContinue)) {
    Write-Error "Exchange Online PowerShell module isn't connected. Run Connect-ExchangeOnline first."
    return
}

if (-not (Test-Path $CsvPath)) {
    Write-Error "CSV file not found at path: $CsvPath"
    return
}

$rows = Import-Csv -Path $CsvPath

if (-not $rows -or $rows.Count -eq 0) {
    Write-Warning "CSV file is empty, nothing to process."
    return
}

$results = foreach ($row in $rows) {
    $mailbox = $row.SharedMailbox
    $user = $row.User

    if ([string]::IsNullOrWhiteSpace($mailbox) -or [string]::IsNullOrWhiteSpace($user)) {
        Write-Warning "Skipping row with a blank SharedMailbox or User value."
        continue
    }

    $mailboxObj = Get-Mailbox -Identity $mailbox -ErrorAction SilentlyContinue
    if (-not $mailboxObj) {
        Write-Warning "Skipping $user, mailbox not found: $mailbox"
        continue
    }

    $userObj = Get-Recipient -Identity $user -ErrorAction SilentlyContinue
    if (-not $userObj) {
        Write-Warning "Skipping $mailbox, user not found: $user"
        continue
    }

    try {
        Add-MailboxPermission -Identity $mailbox -User $user -AccessRights FullAccess -InheritanceType All -AutoMapping $AutoMapping -ErrorAction Stop | Out-Null
        Add-RecipientPermission -Identity $mailbox -Trustee $user -AccessRights SendAs -Confirm:$false -ErrorAction Stop | Out-Null

        [PSCustomObject]@{
            SharedMailbox = $mailbox
            User          = $user
            Status        = "Granted"
        }
    }
    catch {
        Write-Warning "Failed on $mailbox for $user, $($_.Exception.Message)"
        [PSCustomObject]@{
            SharedMailbox = $mailbox
            User          = $user
            Status        = "Failed"
        }
    }
}

$results | Format-Table -AutoSize

$failedCount = ($results | Where-Object { $_.Status -eq "Failed" }).Count
if ($failedCount -gt 0) {
    Write-Warning "$failedCount row(s) failed. Check the warnings above for details."
}
else {
    Write-Host "All rows processed successfully."
}

Read it before you run it, and test in a safe tenant first.