source/00_Main/New-M365UsageReport.ps1

Function New-M365UsageReport {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet(7, 30, 90, 180)]
        [int]
        $ReportPeriod = 7,

        [Parameter()]
        [ValidateSet(
            'Microsoft365',
            'Exchange',
            'DefenderATP',
            'SharePoint',
            'OneDrive',
            'Teams'
        )]
        [string[]]
        $Scope,

        [Parameter()]
        [ValidateSet(
            'Microsoft365AssignedLicenses',
            'Microsoft365ActiveUsers',
            'Microsoft365ProductActivation',
            'Microsoft365Groups',
            'ExchangeMailboxUsageAndProvisioning',
            'ExchangeClientAppUsage',
            'ExchangeMailFlow',
            'ExchangeTop10MailTraffic',
            'DefenderATPDetections',
            'SharePointUsageAndStorage',
            'OneDriveUsageAndStorage',
            'TeamsUsers',
            'TeamsUsersActivities',
            'TeamsDevices'
        )]
        [string[]]
        $Exclude,

        [Parameter()]
        [switch]
        $SendEmail,

        [Parameter()]
        [string]
        $From,

        [Parameter()]
        [string[]]
        $To,

        [Parameter()]
        [string[]]
        $Cc,

        [Parameter()]
        [string[]]
        $Bcc,

        [Parameter()]
        [string]
        $CustomEmailSubject,

        [Parameter()]
        [switch]
        $ShowReport
    )

    if (!$Scope) {
        $Scope = @(
            'Microsoft365',
            'Exchange',
            'DefenderATP',
            'SharePoint',
            'OneDrive',
            'Teams'
        )
    }

    ## Check if Microsoft Graph API is connected
    if (!(IsGraphConnected)) {
        SayError 'Microsoft Graph API PowerShell is not connected.'
        LogEnd
        return $null
    }

    $script:initialCloudDomain = (Get-MgDomain | Where-Object { $_.IsInitial }).id

    ## Set the output folder
    $reportFolder = "$($env:TEMP)\M365UsageReport\$($script:initialCloudDomain)"

    # $logFile = ([System.IO.Path]::Combine($reportFolder, "$($script:initialCloudDomain).log"))
    $logFile = ([System.IO.Path]::Combine($reportFolder, "transcript.log"))

    LogStart $logFile
    SayInfo "Transcript log is saved to '$((Resolve-Path $logFile).Path)'"

    $ProgressPreference = 'SilentlyContinue'

    ## Validate SendEmail parameters
    $isSendEmailError = $false
    if ($SendEmail) {
        if (-not $From) {
            SayError 'The "-From <sender email address>" is required when using the -SendEmail parameter.'
            $isSendEmailError = $true
        }
        if (-not($To) -and -not($Cc) -and -not($Bcc)) {
            SayError 'Specify at least one -To, -Cc, or -Bcc when using the -SendEmail parameter.'
            $isSendEmailError = $true
        }
    }
    if ($isSendEmailError -eq $true) {
        LogEnd
        return $null
    }

    ## Check if required Microsoft Graph API permissions are present.
    $mgContext = Get-MgContext
    $apiPermissions = $mgContext.Scopes
    $permFlag = $true
    if ('Directory.Read.All' -notin $apiPermissions) {
        $permFlag = $false
        SayError 'The access token is missing the {Directory.Read.All} permission'
    }
    if ('Reports.Read.All' -notin $apiPermissions) {
        $permFlag = $false
        SayError 'The access token is missing the {Reports.Read.All} permission'
    }
    if ($SendEmail -and 'Mail.Send' -notin $apiPermissions) {
        $permFlag = $false
        SayError 'The access token is missing the {Mail.Send} permission'
    }
    if (!$permFlag) {
        LogEnd
        return $null
    }

    ## Check if Exchange Online PowerShell is connected.
    if ($IncludeReport -contains 'Exchange' -and $IncludeReport -contains 'DefenderATP') {
        if (!(IsExchangeConnected)) {
            SayError 'Exchange PowerShell is not connected. Connect to Exchange Online PowerShell first and try again.'
            LogEnd
            return $null
        }
    }

    ## Set the report Start and End date based on the available data from Microsoft 365 usage reports.
    $null = SetM365ReportDate -ReportPeriod $ReportPeriod

    ## Get the tenant organization
    $organization = Get-MgOrganization

    ## Set the tenant organization name for the report
    $organizationName = $organization.DisplayName

    ## Retrieve this module's metadata.
    $thisModule = $MyInvocation.MyCommand.Module

    ## Retrieve this module's base path.
    $moduleBase = $thisModule.ModuleBase.ToString()

    ## Set the resource folder
    $resourceFolder = "$($moduleBase)\resource"

    ## Set the resources paths (icons)
    $logoFile = "$($resourceFolder)\logo.png"
    $officeIconFile = "$($resourceFolder)\office.png"
    $exchangeIconFile = "$($resourceFolder)\exchange.png"
    $sharepointIconFile = "$($resourceFolder)\sharepoint.png"
    $onedriveIconFile = "$($resourceFolder)\onedrive.png"
    $teamsIconFile = "$($resourceFolder)\teams.png"
    $settingsIconFile = "$($resourceFolder)\settings.png"
    $defenderIconFile = "$($resourceFolder)\defender.png"

    ## Import the CSS for the HTML report
    $css = $(Get-Content "$($resourceFolder)\style.css" -Raw)

    ## Set the report and email subject
    if (-not ($CustomEmailSubject)) {
        $mailSubject = "[$($organizationName)] Microsoft 365 Usage Report ($ReportPeriod days)"
    }
    else {
        $mailSubject = $CustomEmailSubject
    }

    ## Compose the HTML report
    $html = '<html><head><title>' + $($mailSubject) + '</title>'
    $html += '<style type="text/css">'
    $html += $css
    $html += '</style>'
    $html += '</head><body>'
    $html += '<table id="mainTable">'
    if ($showLogo) {
        $html += '<tr><td class="placeholder"><img src="' + $logoFile + '"></td>'
    }
    $html += '<td class="vl"></td>'
    $html += '<td class="title">' + $organizationName + '<br>' + 'Microsoft 365 Usage Report' + '<br>' + ("{0:MMMM dd, yyyy}" -f $Script:GraphStartDate ) + " to " + ("{0:MMMM dd, yyyy}" -f $Script:GraphEndDate) + '</td></tr>'
    $html += '<tr><td class="placeholder" colspan="3"></td></tr>'
    $html += '</table>'

    #==============================================
    # Licenses Assigned Report
    #==============================================
    if ($Scope -contains 'Microsoft365' -and $Exclude -notcontains 'Microsoft365AssignedLicenses') {
        SayInfo "Microsoft 365 Report: Users and Licenses"
        $raw = Get-M365UserLicenseSummary -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $officeIconFile + '"></th><th class="section">Users and Assigned Licenses</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Total Users</th><td>' + ("{0:N0}" -f $raw.'Total Users') + '</td></tr>'
        $html += '<tr><th>With License</th><td>' + ("{0:N0}" -f $raw.'With License') + '</td></tr>'
        $html += '<tr><th>Without License</th><td>' + ("{0:N0}" -f $raw.'Without License') + '</td></tr>'
        $html += '<tr><th>Has Exchange License</th><td>' + ("{0:N0}" -f $raw.'Has Exchange License') + '</td></tr>'
        $html += '<tr><th>Has Sharepoint License</th><td>' + ("{0:N0}" -f $raw.'Has Sharepoint License') + '</td></tr>'
        $html += '<tr><th>Has OneDrive License</th><td>' + ("{0:N0}" -f $raw.'Has OneDrive License') + '</td></tr>'
        $html += '<tr><th>Has Teams License</th><td>' + ("{0:N0}" -f $raw.'Has Teams License') + '</td></tr>'
        $html += '<tr><th>Has Yammer License</th><td>' + ("{0:N0}" -f $raw.'Has Yammer License') + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr>'
        $html += '</table>'
    }

    #==============================================
    # MS365 Activations Users Count Report
    #==============================================

    if ($Scope -contains 'Microsoft365' -and $Exclude -notcontains 'Microsoft365ProductActivation') {
        SayInfo "Microsoft 365 Report: Microsoft 365 App Activations"
        $raw = Get-M365AppActivationSummary

        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $officeIconFile + '"></th><th class="section">Product Activations</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Product Type</th><th>Assigned</th><th>Activated</th><th>Shared Computer Activation</th></tr>'

        foreach ($detail in $raw) {
            $html += '<tr><th>' + ($detail."product Type") + '</th>
        <td>'
 + ("{0:N0}" -f [int]$detail.assigned) + '</td>
        <td>'
 + ("{0:N0}" -f [int]$detail.activated) + '</td>
        <td>'
 + ("{0:N0}" -f [int]$detail."shared Computer Activation") + '</td>
        </tr>'

        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # MS365 Active Users Count Report
    #==============================================

    if ($Scope -contains 'Microsoft365' -and $Exclude -notcontains 'Microsoft365ActiveUsers') {
        SayInfo "Microsoft 365 Report: Active Users Per Service"
        $raw = Get-M365ActiveUserSummary -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $officeIconFile + '"></th><th class="section">Active Users</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Service</th><th>Active</th><th>Inactive</th></tr>'
        $html += '<tr><th>Office 365</th><td>' + ("{0:N0}" -f [int]$raw.'Office 365 Active') + '</td><td>' + ("{0:N0}" -f [int]$raw.'Office 365 Inactive') + '</td></tr>'
        $html += '<tr><th>Exchange</th><td>' + ("{0:N0}" -f [int]$raw.'Exchange Active') + '</td><td>' + ("{0:N0}" -f [int]$raw.'Exchange Inactive') + '</td></tr>'
        $html += '<tr><th>OneDrive</th><td>' + ("{0:N0}" -f [int]$raw.'OneDrive Active') + '</td><td>' + ("{0:N0}" -f [int]$raw.'OneDrive Inactive') + '</td></tr>'
        $html += '<tr><th>Sharepoint</th><td>' + ("{0:N0}" -f [int]$raw.'SharePoint Active') + '</td><td>' + ("{0:N0}" -f [int]$raw.'SharePoint Inactive') + '</td></tr>'
        $html += '<tr><th>Teams</th><td>' + ("{0:N0}" -f [int]$raw.'Teams Active') + '</td><td>' + ("{0:N0}" -f [int]$raw.'Teams Inactive') + '</td></tr>'
        $html += '<tr><th>Yammer</th><td>' + ("{0:N0}" -f [int]$raw.'Yammer Active') + '</td><td>' + ("{0:N0}" -f [int]$raw.'Yammer Inactive') + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr>'
        $html += '</table>'
    }

    #==============================================
    # MS365 Groups Report
    #==============================================

    if ($Scope -contains 'Microsoft365' -and $Exclude -notcontains 'Microsoft365Groups') {
        SayInfo "Microsoft 365 Report: Microsoft 365 Groups Provisioning"
        $raw = Get-M365GroupProvisioningSummary -ReportPeriod $ReportPeriod

        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $officeIconFile + '"></th><th class="section">Microsoft 365 Groups Provisioning</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Current Groups</th><td>' + ("{0:N0}" -f $raw.Current) + '</td></tr>'
        $html += '<tr><th>Created Groups</th><td>' + ("{0:N0}" -f $raw.Created) + '</td></tr>'
        $html += '<tr><th>Delete Groups</th><td>' + ("{0:N0}" -f $raw.Deleted) + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # Exchange Mailbox Usage and Provisioning Report
    #==============================================
    if ($Scope -contains 'Exchange' -and $Exclude -notcontains 'ExchangeMailboxUsageAndProvisioning') {
        SayInfo "Exchange Online Report: Mailbox Active Status"
        $ExchangeMailboxUsageDetail = Get-ExchangeMailboxUsageDetail -ReportPeriod $ReportPeriod

        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Mailbox Status</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Active Mailbox</th><td>' + ("{0:N0}" -f ($ExchangeMailboxUsageDetail | Where-Object { $_.'Active Status' -eq 'Active' }).Count) + '</td></tr>'
        $html += '<tr><th>Inactive Mailbox</th><td>' + ("{0:N0}" -f ($ExchangeMailboxUsageDetail | Where-Object { $_.'Active Status' -eq 'Inactive' }).Count) + '</td></tr>'
        $html += '</table>'

        SayInfo "Exchange Online Report: Mailbox Provisioning"
        $ExchangeMailboxProvisioningSummary = Get-ExchangeMailboxProvisioningSummary -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Mailbox Provisioning</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Created Mailbox</th><td>' + ("{0:N0}" -f $ExchangeMailboxProvisioningSummary.'Created Mailbox') + '</td></tr>'
        $html += '<tr><th>Deleted Mailbox</th><td>' + ("{0:N0}" -f $ExchangeMailboxProvisioningSummary.'Deleted Mailbox') + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr></table>'

        SayInfo "Exchange Online Report: Mailbox Quota and Tenant Storage"
        $ExchangeMailboxQuotaSummary = (Get-ExchangeMailboxQuotaSummary -ReportPeriod $ReportPeriod)[0]
        $exoStorage = ((Get-ExchangeTenantStorageUsage -ReportPeriod $ReportPeriod)[0])
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Mailbox Storage Usage</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Storage Used (TB)</th><td>' + ("{0:N2}" -f (($exoStorage.'Storage Used (Byte)') / 1TB)) + '</td></tr>'
        $html += '<tr><th>Under 25%</th><td>' + ("{0:N0}" -f ($ExchangeMailboxUsageDetail | Where-Object { $_.'Percent Used' -LT 25 }).Count) + '</td></tr>'
        $html += '<tr><th>Under Limit</th><td>' + ("{0:N0}" -f $ExchangeMailboxQuotaSummary.'under Limit') + '</td></tr>'
        $html += '<tr><th>Warning Issued</th><td>' + ("{0:N0}" -f $ExchangeMailboxQuotaSummary.'Warning Issued') + '</td></tr>'
        $html += '<tr><th>Send Prohibited</th><td>' + ("{0:N0}" -f $ExchangeMailboxQuotaSummary.'Send Prohibited') + '</td></tr>'
        $html += '<tr><th>Send/Receive Prohibited</th><td>' + ("{0:N0}" -f $ExchangeMailboxQuotaSummary.'Send/Receive Prohibited') + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # Exchange Email App Report
    #==============================================
    if ($Scope -contains 'Exchange' -and $Exclude -notcontains 'ExchangeClientAppUsage') {
        SayInfo "Exchange Online Report: Email Apps"
        $raw = Get-ExchangeEmailAppUsageSummary -ReportPeriod $ReportPeriod | Select-Object * -ExcludeProperty Report*, *Date

        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Email Apps Usage</th></tr></table><table id="mainTable">'

        foreach ($item in ($raw.psobject.properties | Sort-Object Name)) {
            $html += '<tr><th>' + $item.Name + '</th><td>' + ("{0:N0}" -f [int]($item.Value)) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # Exchange Email Flow Report
    #==============================================
    if ($Scope -contains 'Exchange' -and $Exclude -notcontains 'ExchangeMailFlow') {
        SayInfo "Exchange Online Report: Mail Flow"
        $raw = Get-ExchangeMailFlowStatus -ReportPeriod $ReportPeriod -Summary

        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Mail Flow Summary</th></tr></table><table id="mainTable">'

        foreach ($item in ($raw.psobject.properties)) {
            $html += '<tr><th>' + $item.Name + '</th><td>' + ("{0:N0}" -f [int]($item.Value)) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # Exchange Top 10 Mail Traffic Report
    #==============================================
    if ($Scope -contains 'Exchange' -and $Exclude -notcontains 'ExchangeTop10MailTraffic') {

        # Top 10 Mail Recipients
        SayInfo "Exchange Online Report: Top 10 Mail Recipients"
        $raw = Get-ExchangeTopMailRecipient -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Top 10 Email Recipients</th></tr></table><table id="mainTable">'
        $html += '<tr><th>ID</th><th>Message Count</th></tr>'
        foreach ($item in $raw) {
            $html += '<tr><th>' + $item.Id + '</th><td>' + ("{0:N0}" -f [int]($item.'Message Count')) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'

        # Top 10 Mail Senders
        SayInfo "Exchange Online Report: Top 10 Mail Senders"
        $raw = Get-ExchangeTopMailSender -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Top 10 Email Senders</th></tr></table><table id="mainTable">'
        $html += '<tr><th>ID</th><th>Message Count</th></tr>'
        foreach ($item in $raw) {
            $html += '<tr><th>' + $item.Id + '</th><td>' + ("{0:N0}" -f [int]($item.'Message Count')) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'

        # Top 10 Spam Recipients
        SayInfo "Exchange Online Report: Top 10 Spam Recipients"
        $raw = Get-ExchangeTopSpamRecipient -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Top 10 Spam Recipients</th></tr></table><table id="mainTable">'
        $html += '<tr><th>ID</th><th>Message Count</th></tr>'
        foreach ($item in $raw) {
            $html += '<tr><th>' + $item.Id + '</th><td>' + ("{0:N0}" -f [int]($item.'Message Count')) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'

        # Top 10 Malware Recipients
        SayInfo "Exchange Online Report: Top 10 Malware Recipients"
        $raw = Get-ExchangeTopMalwareRecipient -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Top 10 Malware Recipients</th></tr></table><table id="mainTable">'
        $html += '<tr><th>ID</th><th>Message Count</th></tr>'
        foreach ($item in $raw) {
            $html += '<tr><th>' + $item.Id + '</th><td>' + ("{0:N0}" -f [int]($item.'Message Count')) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'

        # Top 10 Malware Types
        SayInfo "Exchange Online Report: Top 10 Malware Types"
        $raw = Get-ExchangeTopMalwareDetected -ReportPeriod $ReportPeriod
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $exchangeIconFile + '"></th><th class="section">Top 10 Malware Types</th></tr></table><table id="mainTable">'
        $html += '<tr><th>ID</th><th>Message Count</th></tr>'
        foreach ($item in $raw) {
            $html += '<tr><th>' + $item.Id + '</th><td>' + ("{0:N0}" -f [int]($item.'Message Count')) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # Defender ATP Detections Report
    #==============================================
    if ($Scope -contains 'DefenderATP' -and $Exclude -notcontains 'DefenderATPDetections') {
        SayInfo "Defender ATP Report: SafeLinks and SafeAttachments"
        $raw = Get-ExchangeATPMailDetectionSummary -StartDate $Script:GraphStartDate -EndDate $Script:GraphEndDate
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $defenderIconFile + '"></th><th class="section">Defender ATP Email Detection</th></tr></table><table id="mainTable">'
        foreach ($item in ($raw.psobject.properties)) {
            $html += '<tr><th>' + $item.Name + '</th><td>' + ("{0:N0}" -f [int]($item.Value)) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # Sharepoint Report
    #==============================================
    if ($Scope -contains 'SharePoint' -and $Exclude -notcontains 'SharePointUsageAndStorage') {
        SayInfo "SharePoint Online Report: Sites and Storage"
        $raw = Get-SharePointSiteUsageSummary -ReportPeriod $ReportPeriod | Select-Object * -ExcludeProperty Report*, *Date
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $sharepointIconFile + '"></th><th class="section">Sharepoint Sites and Storage</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Storage Used (TB)</th><td>' + ("{0:N2}" -f ((Get-SharePointTenantStorageUsage -ReportPeriod $ReportPeriod)[0].'Storage Used (Byte)' / 1TB)) + '</td></tr>'
        foreach ($item in ($raw.psobject.properties)) {
            $html += '<tr><th>' + $item.Name + '</th><td>' + ("{0:N0}" -f [int]($item.Value)) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # OneDrive Report
    #==============================================
    if ($Scope -contains 'OneDrive' -and $Exclude -notcontains 'OneDriveUsageAndStorage') {
        SayInfo "OneDrive Report: Accounts and Storage"
        $raw = Get-OneDriveAccountUsageSummary -ReportPeriod $ReportPeriod | Select-Object * -ExcludeProperty Report*, *Date
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $onedriveIconFile + '"></th><th class="section">OneDrive Sites and Storage</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Storage Used (TB)</th><td>' + ("{0:N2}" -f ((Get-OneDriveTenantStorageUsage -ReportPeriod $ReportPeriod)[0].'Storage Used (Byte)' / 1TB)) + '</td></tr>'
        foreach ($item in ($raw.psobject.properties)) {
            $html += '<tr><th>' + $item.Name + '</th><td>' + ("{0:N0}" -f [int]($item.Value)) + '</td></tr>'
        }
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    #==============================================
    # Microsoft Teams Report
    #==============================================

    # Teams Users
    if ($Scope -contains 'Teams' -and $Exclude -notcontains 'TeamsUsers') {
        SayInfo "Teams Report: Teams Users"
        $raw = Get-TeamsUserActivityDetail -ReportPeriod $ReportPeriod
        $raw | Add-Member -MemberType ScriptProperty -Name LastActivityDate -Value { [datetime]$this.'Last Activity Date' }
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $teamsIconFile + '"></th><th class="section">Teams Users</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Teams Users</th><td>' + ("{0:N0}" -f ($raw | Where-Object { $_.'Is Licensed' -eq 'Yes' }).count) + '</td></tr>'
        $html += '<tr><th>Active Teams Users</th><td>' + ("{0:N0}" -f ($raw | Where-Object { $_.'Is Licensed' -eq 'Yes' -and $_.LastActivityDate -ge $Script:GraphStartDate }).count) + '</td></tr>'
        $html += '<tr><th>Inctive Teams Users</th><td>' + ("{0:N0}" -f ($raw | Where-Object { $_.'Is Licensed' -eq 'Yes' -and $_.LastActivityDate -lt $Script:GraphStartDate }).count) + '</td></tr>'
        $html += '<tr><th>Guest Users</th><td>' + ("{0:N0}" -f ($raw | Where-Object { $_.'User Principal Name' -match '#EXT#' }).count) + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    # Teams User Activity
    if ($Scope -contains 'Teams' -and $Exclude -notcontains 'TeamsUsersActivities') {
        SayInfo "Teams Report: Teams Users Activities"
        $raw = Get-TeamsUserActivityCount -ReportPeriod $ReportPeriod | Select-Object * -ExcludeProperty "Report*", 'Audio Duration', 'Video Duration', 'Screen Sharing Duration'
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $teamsIconFile + '"></th><th class="section">Teams User Activity</th></tr></table><table id="mainTable">'
        # foreach ($item in ($raw.psobject.properties)) {
        # $html += '<tr><th>' + $item.Name + '</th><td>' + ("{0:N0}" -f [int]($item.Value)) + '</td></tr>'
        # }\
        $html += '<tr><th>1:1 Calls</th><td>' + ("{0:N0}" -f ($raw.Calls)) + '</td></tr>'
        $html += '<tr><th>Chat Messages</th><td>' + ("{0:N0}" -f ($raw.'Private Chat Messages')) + '</td></tr>'
        $html += '<tr><th>Channel Messages</th><td>' + ("{0:N0}" -f ($raw.'Team Chat Messages' )) + '</td></tr>'
        $html += '<tr><th>Meetings</th><td>' + ("{0:N0}" -f ($raw.'Meetings' )) + '</td></tr>'
        $html += '<tr><th>Audio Duration (Minutes)</th><td>' + ("{0:N0}" -f ($raw.'Audio Duration (Minutes)' )) + '</td></tr>'
        $html += '<tr><th>Video Duration (Minutes)</th><td>' + ("{0:N0}" -f ($raw.'Video Duration (Minutes)' )) + '</td></tr>'
        $html += '<tr><th>Screen Sharing Duration (Minutes)</th><td>' + ("{0:N0}" -f ($raw.'Screen Sharing Duration (Minutes)' )) + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    # Teams Device Usage
    if ($Scope -contains 'Teams' -and $Exclude -notcontains 'TeamsDevices') {
        SayInfo "Teams Report: Teams Devices"
        $raw = Get-TeamsDeviceUsageDistributionSummary -ReportPeriod $ReportPeriod | Select-Object Windows, Mac, Web, iOS, 'Android Phone', 'Windows Phone', 'Chrome OS', 'Linux'
        $html += '<table id="mainTable"><tr><th class="section"><img src="' + $teamsIconFile + '"></th><th class="section">Teams Devices</th></tr></table><table id="mainTable">'
        $html += '<tr><th>Windows</th><td>' + ("{0:N0}" -f [int]($raw.Windows)) + '</td></tr>'
        $html += '<tr><th>Mac</th><td>' + ("{0:N0}" -f [int]($raw.Mac)) + '</td></tr>'
        $html += '<tr><th>Web</th><td>' + ("{0:N0}" -f [int]($raw.Web)) + '</td></tr>'
        $html += '<tr><th>iOS</th><td>' + ("{0:N0}" -f [int]($raw.iOS)) + '</td></tr>'
        $html += '<tr><th>Android Phone</th><td>' + ("{0:N0}" -f [int]($raw.'Android Phone')) + '</td></tr>'
        $html += '<tr><th>Windows Phone</th><td>' + ("{0:N0}" -f [int]($raw.'Windows Phone')) + '</td></tr>'
        $html += '<tr><th>Chrome OS</th><td>' + ("{0:N0}" -f [int]($raw.'Chrome OS')) + '</td></tr>'
        $html += '<tr><th>Linux</th><td>' + ("{0:N0}" -f [int]($raw.Linux)) + '</td></tr>'
        $html += '<tr><td class="placeholder"> </td></tr></table>'
    }

    $html += '<table id="mainTable"><tr><th class="section"><img src="' + $settingsIconFile + '"></th><th class="section">Report Parameters</th></tr></table><table id="mainTable">'
    $html += '<tr><th>Report Period</th><td>' + $ReportPeriod + ' days</td></tr>'
    $html += '<tr><th>Enabled Reports</th><td>' + ($Scope -join ', ') + '</td></tr>'
    $html += '<tr><th>Host</th><td>' + $env:COMPUTERNAME + '</td></tr>'
    $html += '<tr><td colspan="2"><a href="' + ($thisModule.PROJECTURI) + '">' + ($thismodule.Name) + ' v' + ($thismodule.Version) + '</td></tr>'
    $html += '</table>'
    $html += '</body></html>'
    $html = $html -join "`n"
    try {
        $htmlFile = ([System.IO.Path]::Combine($reportFolder, "Microsoft_365_Usage_Report.html"))
        # $null = New-Item -ItemType File -Path $htmlFile -Force -Confirm:$false
        $html | Out-File $htmlFile -Force -Confirm:$false -ErrorAction Stop
        SayInfo "HTML report is saved to '$((Resolve-Path $htmlFile).Path)'"
        if ($ShowReport) {
            Invoke-Item $htmlFile
        }
    }
    catch {
        SayError "$($_.Exception)"
        [System.GC]::Collect()
    }

    $html = $html.Replace($officeIconFile, "cid:officeIconFile")
    $html = $html.Replace("$($exchangeIconFile)", "exchangeIconFile")
    $html = $html.Replace("$($defenderIconFile)", "defenderIconFile")
    $html = $html.Replace("$($sharepointIconFile)", "cid:sharepointIconFile")
    $html = $html.Replace("$($onedriveIconFile)", "cid:onedriveIconFile")
    # $html = $html.Replace("$($skypeIconFile)", "cid:skypeIconFile")
    $html = $html.Replace("$($teamsIconFile)", "cid:teamsIconFile")
    $html = $html.Replace("$($settingsIconFile)", "cid:settingsIconFile")

    ## Send email report
    if ($SendEmail) {
        SayInfo "Sending email report"
        try {
            #message
            $mailParam = @{
                message = @{
                    subject                = $mailSubject
                    body                   = @{
                        contentType = "HTML"
                        content     = $html
                    }
                    internetMessageHeaders = @(
                        @{
                            name  = "X-Mailer"
                            value = "$($thismodule.Name) v$($thismodule.Version)"
                        }
                    )
                    attachments            = @(
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "logoFile"
                            "name"         = "logoFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($logoFile)))"
                        }
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "officeIconFile"
                            "name"         = "officeIconFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($officeIconFile)))"
                        }
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "exchangeIconFile"
                            "name"         = "exchangeIconFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($exchangeIconFile)))"
                        }
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "defenderIconFile"
                            "name"         = "defenderIconFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($defenderIconFile)))"
                        }
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "sharepointIconFile"
                            "name"         = "sharepointIconFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($sharepointIconFile)))"
                        }
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "onedriveIconFile"
                            "name"         = "onedriveIconFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($onedriveIconFile)))"
                        }
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "teamsIconFile"
                            "name"         = "teamsIconFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($teamsIconFile)))"
                        }
                        @{
                            "@odata.type"  = "#microsoft.graph.fileAttachment"
                            "contentID"    = "settingsIconFile"
                            "name"         = "settingsIconFile"
                            "IsInline"     = $true
                            "contentType"  = "image/png"
                            "contentBytes" = "$([convert]::ToBase64String([System.IO.File]::ReadAllBytes($settingsIconFile)))"
                        }
                    )
                }
            }

            # To address
            if ($To) {
                [array]$toAddress = $To.Split(",")
                # create JSON-format recipients
                $toAddressJSON = @()
                $toAddress | ForEach-Object {
                    $toAddressJSON += @{EmailAddress = @{Address = $_ } }
                }
                $mailParam.message += @{
                    toRecipients = @(
                        $ToAddressJSON
                    )
                }
            }

            # Cc address
            if ($Cc) {
                [array]$ccAddress = $Cc.Split(",")
                # create JSON-format recipients
                $ccAddressJSON = @()
                $ccAddress | ForEach-Object {
                    $ccAddressJSON += @{EmailAddress = @{Address = $_ } }
                }
                $mailParam.message += @{
                    ccRecipients = @(
                        $ccAddressJSON
                    )
                }
            }

            # Bcc address
            if ($Bcc) {
                [array]$bccAddress = $Bcc.Split(",")
                # create JSON-format recipients
                $bccAddressJSON = @()
                $bccAddress | ForEach-Object {
                    $bccAddressJSON += @{EmailAddress = @{Address = $_ } }
                }
                $mailParam.message += @{
                    bccRecipients = @(
                        $bccAddressJSON
                    )
                }
            }

            Send-MgUserMail @mailParam -UserId $From -ErrorAction Stop
            SayInfo "Sent!"
        }
        catch {
            SayError "Send failed!"
            SayError "$($_.Exception)"
            [System.GC]::Collect()
            LogEnd
            return $null
        }
    }
    LogEnd
}