Licenses are one of the biggest recurring costs in Microsoft 365. In most tenants, a portion of users remain licensed even though they are no longer active — for example:
These inactive licensed users quietly waste licenses and often show up in cost-optimization reviews.
This script identifies licensed member users who haven’t signed in for the last 90 days (or never signed in), exports an Excel-safe CSV report, and emails it automatically to administrators or licensing stakeholders.
Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.
$SenderUPN = "admin@yourtenant.onmicrosoft.com"
$Recipients = @(
"admin@yourtenant.onmicrosoft.com",
"licensingteam@yourtenant.onmicrosoft.com"
)
$InactiveDays = 90
$CutoffDate = (Get-Date).ToUniversalTime().AddDays(-$InactiveDays)
Connect-MgGraph -Scopes "User.Read.All","Directory.Read.All","AuditLog.Read.All","Mail.Send"
$LicensedUsers = Get-MgUser -All `
-Filter "assignedLicenses/`$count ne 0 and userType eq 'Member' and accountEnabled eq true" `
-ConsistencyLevel eventual `
-CountVariable Records `
-Property Id,DisplayName,UserPrincipalName,Mail,AssignedLicenses,CreatedDateTime,SignInActivity,AccountEnabled
$Report = @()
foreach ($u in $LicensedUsers) {
$LastSignIn = $null
if ($u.SignInActivity -and $u.SignInActivity.LastSignInDateTime) {
$LastSignIn = ([datetime]$u.SignInActivity.LastSignInDateTime).ToUniversalTime()
}
$NeverSignedIn = -not $LastSignIn
$Inactive = $LastSignIn -and ($LastSignIn -lt $CutoffDate)
if ($NeverSignedIn -or $Inactive) {
$InactiveDaysCalc = if ($LastSignIn) {
[int]((Get-Date).ToUniversalTime() - $LastSignIn).TotalDays
} else { "N/A" }
$Report += [PSCustomObject]@{
"User Name" = $u.DisplayName
"User Principal Name" = $u.UserPrincipalName
"User Email" = $u.Mail
"License Count" = $u.AssignedLicenses.Count
"Created Date" = $u.CreatedDateTime
"Last Sign-In" = if ($LastSignIn) { $u.SignInActivity.LastSignInDateTime } else { "Never Signed In" }
"Inactive Days" = $InactiveDaysCalc
"Inactive Status" = if ($NeverSignedIn) { "Never Signed In" } else { "Inactive > $InactiveDays days" }
"User Id" = $u.Id
}
}
}
$ReportPath = "$env:TEMP\Inactive_Licensed_Users_Report.csv"
if ($Report.Count -gt 0) {
$Report | Sort-Object "Inactive Status","User Name" |
Export-Csv -Path $ReportPath -NoTypeInformation -Encoding utf8
} else {
"No inactive licensed users (>$InactiveDays days) or never-signed-in users were found." |
Set-Content -Path $ReportPath -Encoding utf8
}
$Bytes = [System.IO.File]::ReadAllBytes($ReportPath)
$Utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($ReportPath, [System.Text.Encoding]::UTF8.GetString($Bytes), $Utf8Bom)
$Count = $Report.Count
$Subject = "Inactive Licensed Users Report — $(Get-Date -Format 'yyyy-MM-dd')"
$Body = @"
Hello Team,<br><br>
Attached is the <b>Inactive Licensed Users Report</b>.<br>
Users included are licensed accounts that have<br>
1) never signed in, or<br>
2) not signed in for more than <b>$InactiveDays days</b>.<br><br>
Total inactive licensed users found: <b>$Count</b><br><br>
Regards,<br>
Graph PowerShell Automation
"@
$AttachmentContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($ReportPath))
$Attachments = @(
@{
"@odata.type" = "#microsoft.graph.fileAttachment"
Name = "Inactive_Licensed_Users_Report.csv"
ContentBytes = $AttachmentContent
}
)
$ToRecipients = $Recipients | ForEach-Object {
@{ EmailAddress = @{ Address = $_ } }
}
$Message = @{
Message = @{
Subject = $Subject
Body = @{
ContentType = "HTML"
Content = $Body
}
ToRecipients = $ToRecipients
Attachments = $Attachments
}
SaveToSentItems = "true"
}
Send-MgUserMail -UserId $SenderUPN -BodyParameter $Message
Write-Host "Inactive licensed users report emailed successfully." -ForegroundColor Green
| Error | Cause | Solution |
|---|---|---|
| Filter error on assignedLicenses | $count not escaped | Use assignedLicenses/$count ne 0` with ConsistencyLevel eventual. |
| Access denied (403) | Missing consent for permissions. | Admin-consent required for:
|
| Empty report | No inactive licensed users exist. | Valid output; reduce $InactiveDays to validate behavior. |
Inactive licensed users are one of the easiest places to recover Microsoft 365 cost and reduce directory clutter. This Graph PowerShell script gives admins a reliable way to identify those accounts, generate an audit-ready Excel-safe report, and email it to stakeholders automatically. Running it regularly helps ensure licenses stay aligned with real user activity.
© m365corner.com. All Rights Reserved. Design by HTML Codex