Microsoft Graph PowerShell: Bulk Disable Inactive Mailboxes
The MSOnline and AzureAD PowerShell modules are deprecated and headed for retirement. If you’re managing Exchange Online mailbox lifecycle, the microsoft graph powershell bulk disable inactive mailboxes workflow using the Microsoft Graph SDK is the supported replacement. This guide shows you how to find accounts with no sign-in activity for 90 or more days, optionally convert their mailboxes to shared, then bulk-disable the accounts — all with a full pre- and post-action audit trail.
Installing and Connecting Microsoft.Graph PowerShell SDK
Install the SDK from the PowerShell Gallery and connect with the scopes you need. For bulk disable operations you need at minimum User.ReadWrite.All and AuditLog.Read.All. Use -Scopes for delegated (interactive) auth or a certificate/secret for application auth in automation.
# Install if not present
if (-not (Get-Module Microsoft.Graph -ListAvailable)) {
Install-Module Microsoft.Graph -Scope CurrentUser -Force
}
# Connect with required scopes (delegated, browser consent)
Connect-MgGraph -Scopes 'User.ReadWrite.All', 'AuditLog.Read.All', 'Mail.ReadWrite'
# Verify connection
$ctx = Get-MgContext
Write-Host "Connected as $($ctx.Account) to tenant $($ctx.TenantId)"
For unattended automation, replace the -Scopes parameter with -TenantId, -ClientId, and -CertificateThumbprint (or -ClientSecretCredential) to authenticate as an app registration.
Finding Users with No Sign-In for 90 Days via Get-MgUser
The signInActivity property on the user object holds lastSignInDateTime. You must request it explicitly via -Property because Graph does not return it by default.
$cutoff = (Get-Date).AddDays(-90).ToUniversalTime().ToString('o')
# Get licensed users with sign-in activity data
$inactiveUsers = Get-MgUser -All `
-Filter "accountEnabled eq true and userType eq 'Member'" `
-Property 'id,displayName,userPrincipalName,assignedLicenses,signInActivity' |
Where-Object {
# Exclude accounts that never signed in from the null-safe check
$lastSignIn = $_.SignInActivity?.LastSignInDateTime
$lastSignIn -ne $null -and [datetime]$lastSignIn -lt (Get-Date).AddDays(-90)
}
Write-Host "Inactive licensed users (90+ days): $($inactiveUsers.Count)"
Accounts with a null LastSignInDateTime may be brand new or may have never signed in. Handle them separately — do not silently skip them in a production disable run.
Correlating Sign-In Data with Mailbox Last Activity
Sign-in activity alone does not confirm that a mailbox is inactive — users may access mail through shared devices or mobile profiles that authenticate differently. Add an Exchange Online check using Get-EXOMailboxStatistics from the ExchangeOnlineManagement module.
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -ShowBanner:$false
$enriched = foreach ($user in $inactiveUsers) {
$stats = Get-EXOMailboxStatistics -Identity $user.UserPrincipalName `
-Properties LastLogonTime -ErrorAction SilentlyContinue
[PSCustomObject]@{
UPN = $user.UserPrincipalName
DisplayName = $user.DisplayName
LastAADSignIn = $user.SignInActivity?.LastSignInDateTime
LastMailboxLogon = $stats?.LastLogonTime
HasLicense = ($user.AssignedLicenses.Count -gt 0)
}
}
# Only proceed with accounts where both signals confirm inactivity
$confirmed = $enriched | Where-Object {
$_.LastMailboxLogon -eq $null -or
([datetime]$_.LastMailboxLogon) -lt (Get-Date).AddDays(-90)
}
Bulk Disabling Accounts with Update-MgUser
Once you have a confirmed list, disable in bulk with a -WhatIf-equivalent dry run first. The Graph SDK does not natively support -WhatIf, so use a flag variable.
$WhatIf = $true # Set to $false to actually disable
foreach ($user in $confirmed) {
if ($WhatIf) {
Write-Host "[WHATIF] Would disable: $($user.UPN)"
}
else {
try {
Update-MgUser -UserId $user.UPN -AccountEnabled $false -ErrorAction Stop
Write-Host "Disabled: $($user.UPN)"
}
catch {
Write-Warning "Failed to disable $($user.UPN): $_"
}
}
}
Converting Mailboxes to Shared Before Disabling
Disabling a licensed user account does not automatically remove their license or preserve mail access for legal hold. Convert to a shared mailbox first so colleagues can access historical mail and you can remove the paid license.
foreach ($user in $confirmed) {
try {
Set-Mailbox -Identity $user.UPN -Type Shared -ErrorAction Stop
Write-Host "Converted to Shared: $($user.UPN)"
# Remove assigned licenses via Graph
$licenseUpdate = @{ AddLicenses = @(); RemoveLicenses = ($user | Get-MgUserLicenseDetail | Select-Object -ExpandProperty SkuId) }
Set-MgUserLicense -UserId $user.UPN -BodyParameter $licenseUpdate
}
catch {
Write-Warning "Mailbox conversion failed for $($user.UPN): $_"
}
}
Generating a Pre- and Post-Action Audit Report
Export the full list of affected accounts before and after the operation for change-management records.
$date = Get-Date -Format 'yyyy-MM-dd'
# Pre-action snapshot
$confirmed | Export-Csv "C:\Audit\InactiveMailboxes-PRE-$date.csv" -NoTypeInformation -Encoding UTF8
# Post-action verification
$postCheck = foreach ($user in $confirmed) {
$u = Get-MgUser -UserId $user.UPN -Property 'accountEnabled,userPrincipalName' -ErrorAction SilentlyContinue
[PSCustomObject]@{
UPN = $user.UPN
AccountEnabled = $u?.AccountEnabled
DisabledAt = $date
}
}
$postCheck | Export-Csv "C:\Audit\InactiveMailboxes-POST-$date.csv" -NoTypeInformation -Encoding UTF8
Write-Host "Audit reports saved."
Common Errors and Fixes
Insufficient Graph API permissions — missing User.ReadWrite.All scope. If you see “Insufficient privileges to complete the operation”, disconnect and reconnect with the correct scopes: Disconnect-MgGraph then Connect-MgGraph -Scopes 'User.ReadWrite.All','AuditLog.Read.All'. For app-only auth, verify the app registration has the required Application (not Delegated) permissions and that admin consent has been granted in Entra ID.
LastSignInDateTime is null for accounts that never signed in. Use the null-conditional operator $_.SignInActivity?.LastSignInDateTime to avoid “Cannot index into a null array” errors. Track never-signed-in accounts separately — they may be service accounts or onboarding accounts that should be excluded from the disable run.
Related Cmdlets / See Also
- Microsoft Graph PowerShell SDK Guide
- Managing Microsoft 365 Users with PowerShell
- Finding Inactive Active Directory Accounts
Wrapping Up
Migrating mailbox lifecycle management from MSOnline to the Graph SDK is straightforward once you understand the permission model and the signInActivity property quirks. Pair sign-in data with Exchange mailbox statistics for confident inactive-user detection, always run a dry pass first, and keep pre/post audit CSVs for every change window.


