Skip to content
Posts

Search Unified Audit Log for Mailbox Activity

Use two Exchange Online PowerShell scripts to export Unified Audit Log events performed by a mailbox identity or performed to a mailbox object.

Published
Updated
Reading time 7 min
Words 1392

Overview

When investigating mailbox activity in Microsoft 365, it helps to separate two related questions:

  • What did this mailbox identity do?
  • What happened to this mailbox or object?

These two PowerShell scripts search the Unified Audit Log from both directions. Search-UnifiedAuditLog-operations-performed-by.ps1 uses -UserIds to find operations performed by a mailbox identity. Search-UnifiedAuditLog-operations-performed-to.ps1 uses -ObjectIds to find operations performed to or against the mailbox object.

Both scripts connect to Exchange Online, collect large audit result sets in batches, parse the AuditData JSON payload, and export the results to CSV in the same folder as the script. Both scripts accept the mailbox as a required parameter. The performed-by script searches the last seven days by default, and the performed-to script searches the last 90 days by default.

Requirements

  • PowerShell on a workstation that can connect to Exchange Online
  • The ExchangeOnlineManagement PowerShell module
  • A Microsoft 365 account that can run Search-UnifiedAuditLog
  • Audit log permissions, typically through the View-Only Audit Logs or Audit Logs role
  • Unified Audit Log retention that covers the date range you want to search
  • Local permission to write the exported CSV file

Key Capabilities

  • Searches mailbox audit activity by actor with -UserIds
  • Searches mailbox audit activity by target object with -ObjectIds
  • Accepts the mailbox as a required argument in both scripts
  • Uses a unique SessionId and ReturnLargeSet for paged audit log retrieval
  • Requests up to 5,000 records per batch
  • Parses AuditData JSON into review-friendly CSV columns
  • Preserves the original raw audit payload in the CSV
  • Sorts exported events by time
  • Prints result counts after export
  • Uses filesystem-safe export names
  • Groups remaining operations in the performed-by script after filtering noisy events

Download

The Scripts

Operations Performed By

Search-UnifiedAuditLog-operations-performed-by.ps1 is the actor-focused script. It answers, “What did this mailbox identity do?”

The important line is the Search-UnifiedAuditLog call with -UserIds $Mailbox. This version accepts the mailbox identity as a required positional parameter and uses a filesystem-safe mailbox name for the export file. After the events are parsed, the script filters out FileAccessed and MailItemsAccessed so the CSV focuses on less noisy operations. You can add or remove operations in $ExcludedOperations if your investigation needs a different level of detail.

param(
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [string]$Mailbox
)

Import-Module ExchangeOnlineManagement -ErrorAction Stop
Connect-ExchangeOnline -ErrorAction Stop

$StartDate = (Get-Date).AddDays(-7)
$EndDate   = Get-Date

$SafeMailboxName = $Mailbox -replace '[^a-zA-Z0-9._-]', '_'
$ExportPath = Join-Path $PSScriptRoot "$SafeMailboxName-$($StartDate.ToString('yyyy-MM-dd'))-$($EndDate.ToString('yyyy-MM-dd')).csv"

$SessionId = "PerformedByMailboxAudit_$([guid]::NewGuid().ToString())"

$AllResults = @()

do {
    Write-Host "Searching audit log batch... Current count: $($AllResults.Count)"

    $Batch = Search-UnifiedAuditLog `
        -StartDate $StartDate `
        -EndDate $EndDate `
        -UserIds $Mailbox `
        -SessionId $SessionId `
        -SessionCommand ReturnLargeSet `
        -ResultSize 5000 `
        -ErrorAction Stop

    if ($Batch) {
        $AllResults += $Batch
    }

} while ($Batch.Count -gt 0)

$ParsedResults = $AllResults | ForEach-Object {
    $AuditData = $_.AuditData | ConvertFrom-Json

    $Parameters = if ($AuditData.Parameters) {
        ($AuditData.Parameters | ForEach-Object {
            "$($_.Name)=$($_.Value)"
        }) -join "; "
    } else {
        ""
    }

    [PSCustomObject]@{
        Time            = $_.CreationDate
        Operation       = $_.Operations
        Actor           = $_.UserIds
        Workload        = $AuditData.Workload
        RecordType      = $AuditData.RecordType
        ObjectId        = $AuditData.ObjectId
        MailboxOwnerUPN = $AuditData.MailboxOwnerUPN
        MailboxGuid     = $AuditData.MailboxGuid
        ClientIP        = $AuditData.ClientIP
        UserAgent       = $AuditData.UserAgent
        LogonType       = $AuditData.LogonType
        ResultStatus    = $AuditData.ResultStatus
        Parameters      = $Parameters
        RawAuditData    = $_.AuditData
    }
}

$ExcludedOperations = @(
    "FileAccessed",
    "MailItemsAccessed"
)

$FilteredResults = $ParsedResults | Where-Object {
    $OperationName = "$($_.Operation)".Trim()
    $ExcludedOperations -notcontains $OperationName
}

$FilteredResults |
    Sort-Object Time |
    Export-Csv $ExportPath -NoTypeInformation

Write-Host "Export complete: $ExportPath"
Write-Host "Results found before filtering: $($ParsedResults.Count)"
Write-Host "Results found after filtering: $($FilteredResults.Count)"

Write-Host ""
Write-Host "Remaining operations:"
$FilteredResults |
    Group-Object Operation |
    Sort-Object Count -Descending |
    Select-Object Count, Name |
    Format-Table -AutoSize

Operations Performed To

Search-UnifiedAuditLog-operations-performed-to.ps1 is the target-focused script. It answers, “What happened to this mailbox or object?”

The important line is the Search-UnifiedAuditLog call with -ObjectIds $Mailbox. This version accepts the mailbox or object ID as a required positional parameter, searches the last 90 days, and writes a UTF-8 CSV using a filesystem-safe mailbox name. It does not filter operations after parsing, so the exported CSV keeps every returned audit event for the object within the selected date range.

param(
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [string]$Mailbox
)

Import-Module ExchangeOnlineManagement -ErrorAction Stop

Connect-ExchangeOnline -ErrorAction Stop

$StartDate = (Get-Date).AddDays(-90)
$EndDate   = Get-Date

$SafeMailboxName = $Mailbox -replace '[^a-zA-Z0-9._-]', '_'
$ExportPath = Join-Path $PSScriptRoot "$SafeMailboxName-AllOperationsPerformedTo-Last90Days.csv"

$SessionId = "AuditSearch_$([guid]::NewGuid().ToString())"

$AllResults = @()

do {
    Write-Host "Searching audit log batch... Current count: $($AllResults.Count)"

    $Batch = Search-UnifiedAuditLog `
        -StartDate $StartDate `
        -EndDate $EndDate `
        -ObjectIds $Mailbox `
        -SessionId $SessionId `
        -SessionCommand ReturnLargeSet `
        -ResultSize 5000 `
        -ErrorAction Stop

    if ($Batch) {
        $AllResults += $Batch
    }

} while ($Batch.Count -eq 5000)

$ParsedResults = $AllResults | ForEach-Object {
    $AuditData = $_.AuditData | ConvertFrom-Json

    [PSCustomObject]@{
        CreationDate = $_.CreationDate
        Operation    = $_.Operations
        UserIds      = $_.UserIds
        ObjectId     = $AuditData.ObjectId
        Workload     = $_.Workload
        ResultStatus = $_.ResultStatus
        AuditData    = $_.AuditData
    }
}

$ParsedResults |
    Sort-Object CreationDate |
    Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8

Write-Host "Export complete: $ExportPath"
Write-Host "Results found: $($ParsedResults.Count)"

How to Use

  1. Install the Exchange Online module if needed:
Install-Module ExchangeOnlineManagement
  1. Download the script that matches the question you are asking.

  2. Pass the mailbox identity or object ID when you run the script.

.\Search-UnifiedAuditLog-operations-performed-by.ps1 mailbox@domain.com

or:

.\Search-UnifiedAuditLog-operations-performed-to.ps1 mailbox@domain.com
  1. Adjust the date range if the default window is not enough. The performed-by script uses seven days by default, and the performed-to script uses 90 days by default:
$StartDate = (Get-Date).AddDays(-30)
$EndDate   = Get-Date
  1. Run the script from PowerShell:
.\Search-UnifiedAuditLog-operations-performed-by.ps1 mailbox@domain.com

or, for the performed-to script:

.\Search-UnifiedAuditLog-operations-performed-to.ps1 mailbox@domain.com
  1. Authenticate to Exchange Online when prompted.

  2. Review the exported CSV in the script folder. The performed-by script generated name uses a filesystem-safe mailbox value and date range:

mailbox_domain.com-2026-05-07-2026-05-14.csv

The performed-to script generated name uses a filesystem-safe mailbox value and a fixed last-90-days suffix:

mailbox_domain.com-AllOperationsPerformedTo-Last90Days.csv

Example Output

The console output for the performed-by script looks like this after the export completes:

Searching audit log batch... Current count: 0
Export complete: C:\Audit\mailbox_domain.com-2026-05-07-2026-05-14.csv
Results found before filtering: 42
Results found after filtering: 8

Remaining operations:

Count Name
----- ----
    3 Send
    2 UpdateInboxRules
    1 MailboxLogin
    1 Set-Mailbox
    1 HardDelete

The performed-by CSV includes columns like these:

Time,Operation,Actor,Workload,RecordType,ObjectId,MailboxOwnerUPN,MailboxGuid,ClientIP,UserAgent,LogonType,ResultStatus,Parameters,RawAuditData

The performed-to CSV includes columns like these:

CreationDate,Operation,UserIds,ObjectId,Workload,ResultStatus,AuditData

Notes

  • Use the performed-by script when the mailbox identity is the actor you care about.
  • Use the performed-to script when the mailbox or object is the target you care about.
  • The two scripts can return different event sets for the same mailbox because -UserIds and -ObjectIds filter different audit fields.
  • The export path is based on $PSScriptRoot, so the CSV is written next to the script file.
  • Search-UnifiedAuditLog availability depends on Microsoft 365 audit retention, licensing, permissions, and whether auditing was enabled during the period being investigated.
  • Large tenants and broad date ranges can return many events. Start with a narrow window, then expand if needed.
  • MailItemsAccessed and FileAccessed can be high-volume operations. The performed-by script excludes them by default, but you can remove them from $ExcludedOperations if you need the full activity set.

Change Log

2026-05-18

  • Updated Search-UnifiedAuditLog-operations-performed-by.ps1 to accept Mailbox as a required parameter instead of requiring the script file to be edited.
  • Added a filesystem-safe export name to the performed-by script while keeping its date-range suffix.
  • Updated Search-UnifiedAuditLog-operations-performed-to.ps1 to accept Mailbox as a required parameter instead of requiring the script file to be edited.
  • Changed the performed-to default search window from seven days to 90 days.
  • Added a filesystem-safe export name ending in AllOperationsPerformedTo-Last90Days.csv.
  • Simplified the performed-to CSV columns to CreationDate, Operation, UserIds, ObjectId, Workload, ResultStatus, and AuditData.
  • Added -ErrorAction Stop to the performed-to Search-UnifiedAuditLog call and exports the CSV with UTF-8 encoding.

Relationship Map

Connected Memory

This relationship map centers on the current entry and highlights connected categories and tags.

Categories 0
Tags 0
Posts 0