Entra / Microsoft 365 ยท Users & guests
Get graph user statistics
Example of using the Microsoft Graph PowerShell SDK to get usage report data for Entra ID accounts from multiple Microsoft 365 workloads.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -NoWelcome -Scopes User.Read.All, Reports.Read.All, ReportSettings.ReadWrite.All, Mail.Send, Organization.Read.All
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $DestinationEmailAddress = "")$Interactive = $false$ReportName = "Microsoft 365 User Activity Report V2.1"# Determine if we're interactive or notIf ([Environment]::UserInteractive) {# We're running interactively...Clear-HostWrite-Host "Script running interactively... connecting to the Graph" -ForegroundColor YellowConnect-MgGraph -NoWelcome -Scopes User.Read.All, Reports.Read.All, ReportSettings.ReadWrite.All, Mail.Send, Organization.Read.All$Interactive = $true# Email address to use when sending email from interactive session$MsgFrom = (Get-MgContext).Account} Else {# We're not, so likely in Azure AutomationWrite-Output "Executing the runbook to create the last activity report for user member accounts..."Connect-MgGraph -Identity -NoWelcome# Email address to use when sending email from Azure Automation$MsgFrom = "no-reply@office365itpros.com"}# Set the home domain and base OneDrive domain to use when creating OneDrive URLs$HomeDomain = (Get-MgOrganization).VerifiedDomains | Where-Object { $_.IsDefault -eq $true } | Select-Object -ExpandProperty Name$BaseOneDriveDomain = "https://"+$HomeDomain.Split(".")[0]+"-my.sharepoint.com/personal/"# Check that we have the right permissions - in Azure Automation, we assume that the automation account has the right permissionsIf ($Interactive) {[string[]]$CurrentScopes = (Get-MgContext).Scopes[string[]]$RequiredScopes = @('Reports.Read.All','ReportSettings.ReadWrite.All','User.Read.All','Mail.Send','Organization.Read.All')$CheckScopes =[object[]][Linq.Enumerable]::Intersect($RequiredScopes,$CurrentScopes)If ($CheckScopes.Count -ne 5) {Write-Host ("To run this script, you need to connect to Microsoft Graph with the following scopes: {0}" -f $RequiredScopes) -ForegroundColor RedDisconnect-GraphBreak}}$StartTime1 = Get-Date# Define email address to receive the emailed copy of the report# Check if report data is obfuscated in the tenant. If it is, turn obfuscation off so that we get real data for analysis$ObfuscationChanged = $falseIf ((Get-MgAdminReportSetting).DisplayConcealedNames -eq $True) {$Parameters = @{ displayConcealedNames = $False }Update-MgAdminReportSetting -BodyParameter $Parameters$ObfuscationChanged = $true}# Create temporary files to hold usage data downloaded from the Graph$TeamsDataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\TeamsData.csv"If (Test-Path $TeamsDataFile) {Remove-Item $TeamsDataFile -ErrorAction SilentlyContinue}$OneDriveDataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\OneDriveData.csv"If (Test-Path $OneDriveDataFile) {Remove-Item $OneDriveDataFile -ErrorAction SilentlyContinue}$EmailDataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\EmailData.csv"If (Test-Path $EmailDataFile) {Remove-Item $EmailDataFile -ErrorAction SilentlyContinue}$EmailStorageDataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\EmailStorageData.csv"If (Test-Path $EmailStorageDataFile) {Remove-Item $EmailStorageDataFile -ErrorAction SilentlyContinue}$SPODataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\SPOData.csv"If (Test-Path $SPODataFile) {Remove-Item $SPODataFile -ErrorAction SilentlyContinue}$YammerDataFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\YammerData.csv"If (Test-Path $YammerDataFile) {Remove-Item $YammerDataFile -ErrorAction SilentlyContinue}# Download the usage data and store each set in an array# Fetch Teams user activity dataWrite-Output "Fetching Teams user activity data for the last 180 days..."[array]$TeamsUserData = $null$TeamsUserReportsURI = "https://graph.microsoft.com/v1.0/reports/getTeamsUserActivityUserDetail(period='D180')"Try {Invoke-MgGraphRequest -Uri $TeamsUserReportsURI -Method Get -OutputFilePath $TeamsDataFile[array]$TeamsUserData = Import-Csv -Path $TeamsDataFile} Catch {Write-Output "Error fetching Teams user activity data: $_"}# Fetch OneDrive user activity dataWrite-Output "Fetching OneDrive user activity data for the last 180 days..."[array]$OneDriveData = $null$OneDriveUsageUri = "https://graph.microsoft.com/v1.0/reports/getOneDriveUsageAccountDetail(period='D180')"Try {Invoke-MgGraphRequest -Uri $OneDriveUsageURI -Method Get -OutputFilePath $OneDriveDataFile[array]$OneDriveData = Import-Csv -Path $OneDriveDataFile} Catch {Write-Output "Error fetching OneDrive user activity data: $_"}# Fetch Exchange Online dataWrite-Output "Fetching Exchange email activity data for the last 180 days..."[array]$EmailData = $null$EmailReportsUri = "https://graph.microsoft.com/v1.0/reports/getEmailActivityUserDetail(period='D180')"Try {Invoke-MgGraphRequest -Uri $EmailReportsURI -Method Get -OutputFilePath $EmailDataFile[array]$EmailData = Import-Csv -Path $EmailDataFile} Catch {Write-Output "Error fetching Exchange email activity data: $_"}# Fetch mailbox usage dataWrite-Output "Fetching Exchange mailbox usage data for the last 180 days..."$MailboxUsageReportsUri = "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D180')"[array]$MailboxData = $nullTry {Invoke-MgGraphRequest -Uri $MailboxUsageReportsURI -Method Get -OutputFilePath $EmailStorageDataFile[array]$MailboxData = Import-Csv -Path $EmailStorageDataFile} Catch {Write-Output "Error fetching Exchange mailbox usage data: $_"}# Get SharePoint usage dataWrite-Host "Fetching SharePoint Online user activity data from the Graph..."$SPOUsageReportsUri = "https://graph.microsoft.com/v1.0/reports/getSharePointActivityUserDetail(period='D180')"[array]$SPOData = $nullTry {Invoke-MgGraphRequest -Uri $SPOUsageReportsUri -Method Get -OutputFilePath $SPODataFile[array]$SPOData = Import-Csv -Path $SPODataFile} Catch {Write-Output "Error fetching SharePoint Online user activity data: $_"}Write-Host "Fetching Viva Engage user activity data from the Graph..."# Get Viva Engage usage data$YammerUsageReportsUri = "https://graph.microsoft.com/v1.0/reports/getYammerActivityUserDetail(period='D180')"[array]$YammerData = $nullTry {Invoke-MgGraphRequest -Uri $YammerUsageReportsUri -Method Get -OutputFilePath $YammerDataFile[array]$YammerData = Import-Csv -Path $YammerDataFile} Catch {Write-Output "Error fetching Viva Engage user activity data: $_"}# Create the hash table for the usage data fetched for each account$DataTable = @{}# Get User sign in dataWrite-Output "Fetching user account data from the Graph..."Try {[array]$Users = Get-MgUser -All -PageSize 500 -filter "userType eq 'Member'" `-Property DisplayName, UserPrincipalName, signInActivity, Mail, Id, CreatedDateTime, AccountEnabled} Catch {Write-Output "Error fetching user sign-in data: $_"}# If necessary, reset tenant obfuscation settings to TrueIf ($ObfuscationChanged) {If ((Get-MgAdminReportSetting).DisplayConcealedNames -eq $False) {$Parameters = @{ displayConcealedNames = $True }Update-MgAdminReportSetting -BodyParameter $Parameters}}$StartTime2 = Get-DateWrite-Output "Processing activity data fetched from the Graph..."# Process Teams DataForEach ($T in $TeamsUserData) {If ([string]::IsNullOrEmpty($T."Last Activity Date")) {$TeamsLastActivity = "No activity"$TeamsDaysSinceActive = "N/A"} Else {$TeamsLastActivity = Get-Date($T."Last Activity Date") -format "dd-MMM-yyyy"$TeamsDaysSinceActive = (New-TimeSpan($TeamsLastActivity)).Days}$ReportLine = [PSCustomObject] @{TeamsUPN = $T."User Principal Name"TeamsLastActive = $TeamsLastActivityTeamsDaysSinceActive = $TeamsDaysSinceActiveTeamsReportDate = Get-Date($T."Report Refresh Date") -format "dd-MMM-yyyy"TeamsLicense = $T."Assigned Products"TeamsChannelChats = $T."Team Chat Message Count"TeamsPrivateChats = $T."Private Chat Message Count"TeamsCalls = $T."Call Count"TeamsMeetings = $T."Meeting Count"TeamsRecordType = "Teams"}$DataTable[$T."User Principal Name"] = $ReportLine}# Process Exchange DataForEach ($E in $EmailData) {$ExoDaysSinceActive = $NullIf ([string]::IsNullOrEmpty($E."Last Activity Date")) {$ExoLastActivity = "No activity"$ExoDaysSinceActive = "N/A"} Else {$ExoLastActivity = Get-Date($E."Last Activity Date") -format "dd-MMM-yyyy"$ExoDaysSinceActive = (New-TimeSpan($ExoLastActivity)).Days}$ReportLine = [PSCustomObject] @{ExoUPN = $E."User Principal Name"ExoDisplayName = $E."Display Name"ExoLastActive = $ExoLastActivityExoDaysSinceActive = $ExoDaysSinceActiveExoReportDate = Get-Date($E."Report Refresh Date") -format "dd-MMM-yyyy"ExoSendCount = [int]$E."Send Count"ExoReadCount = [int]$E."Read Count"ExoReceiveCount = [int]$E."Receive Count"ExoIsDeleted = $E."Is Deleted"ExoRecordType = "Exchange Activity"}[Array]$ExistingData = $DataTable[$E."User Principal Name"][Array]$NewData = $ExistingData + $ReportLine$DataTable[$E."User Principal Name"] = $NewData}ForEach ($M in $MailboxData) {If ([string]::IsNullOrEmpty($M."Last Activity Date")) {$ExoLastActivity = "No activity"} Else {$ExoLastActivity = Get-Date($M."Last Activity Date") -format "dd-MMM-yyyy"$ExoDaysSinceActive = (New-TimeSpan($ExoLastActivity)).Days}$ReportLine = [PSCustomObject] @{MbxUPN = $M."User Principal Name"MbxDisplayName = $M."Display Name"MbxLastActive = $ExoLastActivityMbxDaysSinceActive = $ExoDaysSinceActiveMbxReportDate = Get-Date($M."Report Refresh Date") -format "dd-MMM-yyyy"MbxQuotaUsed = [Math]::Round($M."Storage Used (Byte)"/1GB,2)MbxQuotaPercentUsed = ($M.'Storage Used (Byte)'/$M.'Prohibit Send/Receive Quota (Byte)').toString('P')MbxItems = [int]$M."Item Count"MbxRecordType = "Exchange Storage"}[Array]$ExistingData = $DataTable[$M."User Principal Name"][Array]$NewData = $ExistingData + $ReportLine$DataTable[$M."User Principal Name"] = $NewData}# SharePoint dataForEach ($S in $SPOData) {If ([string]::IsNullOrEmpty($S."Last Activity Date")) {$SPOLastActivity = "No activity"$SPODaysSinceActive = "N/A"} Else {$SPOLastActivity = Get-Date($S."Last Activity Date") -format "dd-MMM-yyyy"$SPODaysSinceActive = (New-TimeSpan ($SPOLastActivity)).Days}$ReportLine = [PSCustomObject] @{SPOUPN = $S."User Principal Name"SPOLastActive = $SPOLastActivitySPODaysSinceActive = $SPODaysSinceActiveSPOViewedEdited = [int]$S."Viewed or Edited File Count"SPOSyncedFileCount = [int]$S."Synced File Count"SPOSharedExt = [int]$S."Shared Externally File Count"SPOSharedInt = [int]$S."Shared Internally File Count"SPOVisitedPages = [int]$S."Visited Page Count"SPORecordType = "SharePoint Usage"}[Array]$ExistingData = $DataTable[$S."User Principal Name"][Array]$NewData = $ExistingData + $ReportLine$DataTable[$S."User Principal Name"] = $NewData}# OneDrive for Business dataForEach ($O in $OneDriveData) {$OneDriveLastActivity = $NullIf ([string]::IsNullOrEmpty($O."Last Activity Date")) {$OneDriveLastActivity = "No activity"$OneDriveDaysSinceActive = "N/A"} Else {$OneDriveLastActivity = Get-Date($O."Last Activity Date") -format "dd-MMM-yyyy"$OneDriveDaysSinceActive = (New-TimeSpan($OneDriveLastActivity)).Days}If ($Interactive) {$ODSite = $O."Owner Principal Name" -replace "@","_" -replace "\.","_"$ODSite = $BaseOneDriveDomain + $ODSite} Else {# Use the Graph to find the OneDrive site URL and return in the same format as the interactive version# This code is overkill, and it works fine in app-only mode but the Get-MgUserDefaultDrive cmdlet fails in an Azure Automation runbook. The fix# is to simply use the $BaseOneDriveDomain + the UPN with substitutions as done in the interactive code above.$User = $O."Owner Principal Name".trim()$UserId = Get-MgUser -UserId $User | Select-Object -ExpandProperty Id$ODSite = ((Get-MgUserDefaultDrive -UserId $UserId).WebUrl).Split("/Documents")[0]}$ReportLine = [PSCustomObject] @{ODUPN = $O."Owner Principal Name"ODDisplayName = $O."Owner Display Name"ODLastActive = $OneDriveLastActivityODDaysSinceActive = $OneDriveDaysSinceActiveODSite = $ODSiteODFileCount = [int]$O."File Count"ODStorageUsed = [Math]::Round($O."Storage Used (Byte)"/1GB,4)ODQuota = [Math]::Round($O."Storage Allocated (Byte)"/1GB,2)ODRecordType = "OneDrive Storage"}[Array]$ExistingData = $DataTable[$O."Owner Principal Name"][Array]$NewData = $ExistingData + $ReportLine$DataTable[$O."Owner Principal Name"] = $NewData}# Yammer DataForEach ($Y in $YammerUsage) {If ([string]::IsNullOrEmpty($Y."Last Activity Date")) {$YammerLastActivity = "No activity"$YammerDaysSinceActive = "N/A"} Else {$YammerLastActivity = Get-Date($Y."Last Activity Date") -format "dd-MMM-yyyy"$YammerDaysSinceActive = (New-TimeSpan ($YammerLastActivity)).Days}$ReportLine = [PSCustomObject] @{YUPN = $Y."User Principal Name"YDisplayName = $Y."Display Name"YLastActive = $YammerLastActivityYDaysSinceActive = $YammerDaysSinceActiveYPostedCount = [int]$Y."Posted Count"YReadCount = [int]$Y."Read Count"YLikedCount = [int]$Y."Liked Count"YRecordType = "Yammer Usage"}[Array]$ExistingData = $DataTable[$Y."User Principal Name"][Array]$NewData = $ExistingData + $ReportLine$DataTable[$Y."User Principal Name"] = $NewData}$StartTime3 = Get-Date# Set up progress barIf ($Interactive) {$ProgressDelta = 100/($Users.Count); $PercentComplete = 0; $UserNumber = 0}$OutData = [System.Collections.Generic.List[Object]]::new() # Create merged output file# Process each user to extract Exchange, Teams, OneDrive, SharePoint, and Yammer (Viva Engage) statistics for their activityForEach ($User in $Users) {$U = $User.UserPrincipalName$UserNumber++If ($Interactive) {$CurrentStatus = $U + " ["+ $UserNumber +"/" + $Users.Count + "]"Write-Progress -Activity "Extracting information for user" -Status $CurrentStatus -PercentComplete $PercentComplete$PercentComplete += $ProgressDelta}$UserData = $DataTable[$U] # Extract data for the user - everything is in a single keyed access to the hash table# Process Exchange Data[string]$ExoUPN = (Out-String -InputObject $UserData.ExoUPN).Trim()[string]$ExoLastActive = (Out-String -InputObject $UserData.ExoLastActive).Trim()If ([string]::IsNullOrEmpty($ExoUPN) -or $ExoLastActive -eq "No Activity") {$ExoDaysSinceActive = "N/A"$ExoLastActive = "No Activity"} Else {[string]$ExoLastActive = (Out-String -InputObject $UserData.ExoLastActive).Trim()[string]$ExoDaysSinceActive = (Out-String -InputObject $UserData.ExoDaysSinceActive).Trim()}# Parse OneDrive for Business usage data[string]$ODUPN = (Out-String -InputObject $UserData.ODUPN).Trim()[string]$ODLastActive = (Out-String -InputObject $UserData.ODLastActive).Trim() # Possibility of a second OneDrive account for some users.If (($ODLastActive -Like "*No Activity*") -or ([string]::IsNullOrEmpty($ODLastActive))) {$ODLastActive = "No Activity"} # this is a hack until I figure out a better way to handle the situationIf ($null -eq [string]::IsNullOrEmpty($ODUPN) -or $ODLastActive -eq "No Activity") {[string]$ODDaysSinceActive = "N/A"[string]$ODLastActive = "No Activity"$ODFiles = 0$ODStorage = 0$ODQuota = 1024} Else {[string]$ODDaysSinceActive = (Out-String -InputObject $UserData.ODDaysSinceActive).Trim()[string]$ODLastActive = (Out-String -InputObject $UserData.ODLastActive).Trim()[string]$ODFiles = (Out-String -InputObject $UserData.ODFileCount).Trim()[string]$ODStorage = (Out-String -InputObject $UserData.ODStorageUsed).Trim()[string]$ODQuota = (Out-String -InputObject $UserData.ODQuota).Trim()}# Parse Yammer usage data; Yammer isn't used everywhere, so make sure that we record zero data[string]$YUPN = (Out-String -InputObject $UserData.YUPN).Trim()[string]$YammerLastActive = (Out-String -InputObject $UserData.YLastActive).Trim()If (([string]::IsNullOrEmpty($YUPN) -or ($YammerLastActive -eq "No Activity"))) {[string]$YammerLastActive = "No Activity"[string]$YammerDaysSinceActive = "N/A"$YammerPosts = 0$YammerReads = 0$YammerLikes = 0} Else {$YammerDaysSinceActive = (Out-String -InputObject $UserData.YDaysSinceActive).Trim()$YammerPosts = (Out-String -InputObject $UserData.YPostedCount).Trim()$YammerReads = (Out-String -InputObject $UserData.YReadCount).Trim()$YammerLikes = (Out-String -InputObject $UserData.YLikedCount).Trim()}If ($UserData.TeamsDaysSinceActive -gt 0) {[string]$TeamsDaysSinceActive = (Out-String -InputObject $UserData.TeamsDaysSinceActive).Trim()[string]$TeamsLastActive = (Out-String -InputObject $UserData.TeamsLastActive).Trim()} Else {[string]$TeamsDaysSinceActive = "N/A"[string]$TeamsLastActive = "No Activity"}If ($UserData.SPODaysSinceActive -gt 0) {[string]$SPODaysSinceActive = (Out-String -InputObject $UserData.SPODaysSinceActive).Trim()[string]$SPOLastActive = (Out-String -InputObject $UserData.SPOLastActive).Trim()} Else {[string]$SPODaysSinceActive = "N/A"[string]$SPOLastActive = "No Activity"}# Fetch the sign in data if available$LastAccountSignIn = $Null; $DaysSinceSignIn = 0If ($null -eq $User.SignInActivity.LastSuccessfulSignInDateTime) {$LastAccountSignIn = "No sign in data found"; $DaysSinceSignIn = "N/A"} Else {$LastAccountSignIn = Get-Date($User.SignInActivity.LastSuccessfulSignInDateTime) -format 'dd-MMM-yyyy HH:mm'$DaysSinceSignIn = (New-TimeSpan($User.SignInActivity.LastSuccessfulSignInDateTime)).Days}# Figure out if the account is used[int]$ExoDays = 365; [int]$TeamsDays = 365; [int]$SPODays = 365; [int]$ODDays = 365; [int]$YammerDays = 365# Base is 2 if someuse uses the five workloads because the Graph is usually 2 days behind, but we have some N/A values for days usedIf ($ExoDaysSinceActive -ne "N/A") {$ExoDays = $ExoDaysSinceActive -as [int]}If ($TeamsDaysSinceActive -eq "N/A") {$TeamsDays = 365} Else {$TeamsDays = $TeamsDaysSinceActive -as [int]}If ($SPODaysSinceActive -eq "N/A") {$SPODays = 365} Else {$SPODays = $SPODaysSinceActive -as [int]}If ($ODDaysSinceActive -eq "N/A") {$ODDays = 365} Else {$ODDays = $ODDaysSinceActive -as [int]}If ($YammerDaysSinceActive -eq "N/A") {$YammerDays = 365} Else {$YammerDays = $YammerDaysSinceActive -as [int]}# Average days per workload used...$AverageDaysSinceUse = [Math]::Round((($ExoDays + $TeamsDays + $SPODays + $ODDays + $YammerDays)/5),2)# Add icons to account statusSwitch ($AverageDaysSinceUse) { # Figure out if account is used({$PSItem -le 12}) { $AccountStatus = "๐ฉ๐ช Heavy usage" }({$PSItem -ge 13 -and $PSItem -le 50} ) { $AccountStatus = "๐จ๐ Moderate usage" }({$PSItem -ge 51 -and $PSItem -le 120} ) { $AccountStatus = "๐ง๐ Poor usage" }({$PSItem -ge 121 -and $PSItem -le 300 } ) { $AccountStatus = "๐ฆ๐ง Review account" }default { $AccountStatus = "๐ฅ๐ซ Account unused" }} # End Switch# And an override if someone has been active in just one workload in the last 14 days[int]$DaysCheck = 14 # Set this to your chosen value if you want to use a different period.If (($ExoDays -le $DaysCheck) -or ($TeamsDays -le $DaysCheck) -or ($SPODays -le $DaysCheck) -or ($ODDays -le $DaysCheck) -or ($YammerDays -le $DaysCheck)) {$AccountStatus = "๐ข๐ช Account in use"}If ((![string]::IsNullOrEmpty($ExoUPN))) {# Build a line for the report file with the collected data for all workloads and write it to the list$OutLine = [PSCustomObject] @{UPN = $UDisplayName = (Out-String -InputObject $UserData.ExoDisplayName).Trim()'Active Status' = $AccountStatus'Last successful sign in' = $LastAccountSignIn'Days since sign in' = $DaysSinceSignIn'Last active in EXO' = $ExoLastActive'Days since active in EXO' = $ExoDaysSinceActive'EXO Quota used' = (Out-String -InputObject $UserData.MbxQuotaUsed).Trim() + " GB"'EXO Quota % used' = (Out-String -InputObject $UserData.MbxQuotaPercentUsed).Trim()'Items in EXO mailbox' = (Out-String -InputObject $UserData.MbxItems).Trim()'EXO messages sent' = (Out-String -InputObject $UserData.ExoSendCount).Trim()'EXO messages read' = (Out-String -InputObject $UserData.ExoReadCount).Trim()'EXO messages received' = (Out-String -InputObject $UserData.ExoReceiveCount).Trim()'Last active in Teams' = $TeamsLastActive'Days since active in Teams' = $TeamsDays'Number of channel messages' = (Out-String -InputObject $UserData.TeamsChannelChats).Trim()'Number of chat messages' = (Out-String -InputObject $UserData.TeamsPrivateChats).Trim()'Number of meetings' = (Out-String -InputObject $UserData.TeamsMeetings).Trim()'Number of Teams calls' = (Out-String -InputObject $UserData.TeamsCalls).Trim()'Last active in SPO' = $SPOLastActive'Days since last active in SPO' = $SPODays'Number of SPO files edited' = (Out-String -InputObject $UserData.SPOViewedEdited).Trim()'Number of SPO files synced' = (Out-String -InputObject $UserData.SPOSyncedFileCount).Trim()'Number of SPO files shared (Ext)' = (Out-String -InputObject $UserData.SPOSharedExt).Trim()'Number of SPO files shared (int)' = (Out-String -InputObject $UserData.SPOSharedInt).Trim()'Number of SPO pages visited' = (Out-String -InputObject $UserData.SPOVisitedPages).Trim()'Last active in OneDrive' = $ODLastActive'Days since last active in OneDrive' = $ODDaysSinceActive'Number of OneDrive files' = $ODFiles'OneDrive storage used' = $ODStorage + " GB"'OneDrive quota assigned' = $ODQuota + " GB"'OneDrive quota used' = ($ODStorage/$ODQuota).ToString("P")'Last active in Viva Engage' = $YammerLastActive'Days since last active in Viva Engage' = $YammerDaysSinceActive'Number of Viva Engage posts' = $YammerPosts'Number of Viva Engage reads' = $YammerReads'Number of Viva Engage reactions' = $YammerLikes'Assigned licenses' = (Out-String -InputObject $UserData.TeamsLicense).Trim()'OneDrive site URL' = (Out-String -InputObject $UserData.ODSite).Trim()'EXO data from' = (Out-String -InputObject $UserData.ExoReportDate).Trim()'Teams data from' = (Out-String -InputObject $UserData.TeamsReportDate).Trim()'Average days since use' = $AverageDaysSinceUse'Account created date' = Get-Date $User.CreatedDateTime -Format 'dd-MMM-yyyy HH:mm''Account enabled' = $User.AccountEnabled}$OutData.Add($OutLine)}} #End processing user data#Clear-Host$StartTime4 = Get-Date$GraphTime = $StartTime2 - $StartTime1$PrepTime = $StartTime3 - $StartTime2$ReportTime = $StartTime4 - $StartTime3$ScriptTime = $StartTime4 - $StartTime1$AccountsPerMinute = [math]::Round(($Outdata.count/($ScriptTime.TotalSeconds/60)),2)$GraphElapsed = "{0}:{1:D2}" -f $GraphTime.Minutes, $GraphTime.Seconds$PrepElapsed = "{0}:{1:D2}" -f $Preptime.Minutes, $Preptime.Seconds$ReportElapsed = "{0}:{1:D2}" -f $ReportTime.Minutes, $ReportTime.Seconds$ScriptElapsed = "{0}:{1:D2}" -f $ScriptTime.Minutes, $ScriptTime.SecondsWrite-Output " "Write-Output ("Statistics for {0}" -f $ReportName)Write-Output "------------------------------------------------------"Write-Output ("Time to fetch data from Microsoft Graph: {0}" -f $GraphElapsed)Write-Output ("Time to prepare date for processing: {0}" -f $PrepElapsed)Write-Output ("Time to create report from data: {0}" -f $ReportElapsed)Write-Output ("Total time for script: {0}" -f $ScriptElapsed)Write-Output ("Total accounts processed: {0}" -f $Outdata.count)Write-Output ("Account processing rate per minute: {0}" -f $AccountsPerMinute)Write-Output " "If ($Interactive) {$OutData | Sort-Object {$_.ExoLastActive -as [DateTime]} -Descending | Out-GridView -Title "User Activity Summary"}$OutData = $OutData | Sort-Object UPN# And generate an output fileIf (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $trueImport-Module ImportExcel -ErrorAction SilentlyContinue$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\AccountActivitySummary.xlsx"If (Test-Path $ExcelOutputFile) {Remove-Item $ExcelOutputFile -ErrorAction SilentlyContinue}$OutData | Export-Excel -Path $ExcelOutputFile -WorksheetName "User Activity Report" -Title ("User Activity Report {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "AccountActivity"$AttachmentFile = $ExcelOutputFile} Else {$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\AccountActivitySummary.CSV"$OutData | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8$AttachmentFile = $CSVOutputFile}If ($ExcelGenerated) {Write-Output ("Excel worksheet output written to {0}" -f $ExcelOutputFile)} Else {Write-Output ("CSV output file written to {0}" -f $CSVOutputFile)}# Prepare to send the email# Encount the output file to an email$EncodedAttachmentFile = [Convert]::ToBase64String([IO.File]::ReadAllBytes($AttachmentFile))$MsgAttachments = @(@{'@odata.type' = '#microsoft.graph.fileAttachment'Name = (Split-Path $AttachmentFile -Leaf)ContentBytes = $EncodedAttachmentFileContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})# Build the array of a single TO recipient detailed in a hash table - change this to the appropriate recipient for your tenant$ToRecipient = @{}$ToRecipient.Add("emailAddress",@{'address'=$DestinationEmailAddress})[array]$MsgTo = $ToRecipient# Define the message subject$MsgSubject = "Important: User Activity Summary Report"# Create the HTML content$HtmlMsg = "</body></html><p>The output file for the <b>User Activity Summary Report</b> is attached to this message. Please review the information at your convenience</p>"# Construct the message body$MsgBody = @{}$MsgBody.Add('Content', "$($HtmlMsg)")$MsgBody.Add('ContentType','html')# Build the parameters to submit the message$Message = @{}$Message.Add('subject', $MsgSubject)$Message.Add('toRecipients', $MsgTo)$Message.Add('body', $MsgBody)$Message.Add("attachments", $MsgAttachments)$EmailParameters = @{}$EmailParameters.Add('message', $Message)$EmailParameters.Add('saveToSentItems', $true)$EmailParameters.Add('isDeliveryReceiptRequested', $true)# Send the messageTry {Send-MgUserMail -UserId $MsgFrom -BodyParameter $EmailParameters -ErrorAction StopWrite-Output ("Inactive guest account report emailed to {0}" -f $ToRecipient.emailAddress.address)} Catch {Write-Output "Unable to send email"Write-Output $_.Exception.Message}Write-Output "All done!"
Parameters
ParameterDefaultNotes
-DestinationEmailAddress""Email address that receives the generated report.Attribution
Author
Office365itpros