Files
m365-administration/message-counter.ps1
2025-09-22 21:13:21 +00:00

196 lines
7.7 KiB
PowerShell

<#
.SYNOPSIS
Interactive mailbox message counter for Microsoft 365 (Graph).
.DESCRIPTION
Prompts the user to choose "my mailbox" or "another/shared mailbox", gathers the target mailbox,
start date, and end date, then connects to Microsoft Graph with the appropriate delegated scope
and returns a count of RECEIVED messages (excluding drafts and self-sent) within the specified
local time window. It searches ALL folders (Inbox, subfolders, Junk, Deleted Items, etc.) so
post-delivery moves don't matter.
Time boundaries are created using the specified Windows time zone (default: "Eastern Standard Time"
for Toronto). The script computes the correct UTC offset for the provided dates (including DST)
and generates ISO 8601 timestamps with offsets (e.g., 2025-01-01T00:00:00-05:00).
.REQUIREMENTS
- PowerShell 5.1+ (Windows) or PowerShell 7+
- Microsoft.Graph PowerShell SDK (will auto-install for CurrentUser if missing)
- For "another/shared mailbox":
* Your signed-in account must have Full Access to that mailbox in Exchange Online.
.PARAMETERS (prompted)
- Scope mode: "1 = My mailbox" or "2 = Another/shared mailbox"
- Mailbox SMTP address (e.g., user@domain.com)
- Start date (YYYY-MM-DD), inclusive
- End date (YYYY-MM-DD), exclusive
.OUTPUTS
Prints a single integer count and a short summary of inputs.
.NOTES
Example month window:
Start: 2025-01-01
End: 2025-02-01
These will be interpreted at 00:00 local time of the configured time zone.
(c) 2025 Robbie Ferguson. All rights reserved.
#>
# --------------------------- User-editable settings ---------------------------
# Windows Time Zone ID (use `tzutil /l` to list). "Eastern Standard Time" covers Toronto with DST.
$TimeZoneId = 'Eastern Standard Time'
# -----------------------------------------------------------------------------
# Helper: ensure Microsoft Graph SDK is available
function Ensure-GraphModule {
$moduleName = 'Microsoft.Graph'
if (-not (Get-Module -ListAvailable -Name $moduleName)) {
Write-Host "Microsoft.Graph module not found. Installing for CurrentUser..."
try {
Set-PSRepository PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
Install-Module $moduleName -Scope CurrentUser -Force -ErrorAction Stop
} catch {
Write-Error "Failed to install Microsoft.Graph: $($_.Exception.Message)"
Read-Host "Press Enter to exit"
exit 1
}
}
Import-Module $moduleName -ErrorAction Stop
}
# Helper: get TimeZoneInfo safely
function Get-TimeZoneInfoSafe {
param([string]$Id)
try {
return [System.TimeZoneInfo]::FindSystemTimeZoneById($Id)
} catch {
Write-Error "Time zone '$Id' not found. Edit `$TimeZoneId at the top (Windows TZ IDs, e.g., 'Eastern Standard Time')."
Read-Host "Press Enter to exit"
exit 1
}
}
# Helper: build ISO timestamp with correct offset for a date at 00:00 local time
function Get-ISOStartOfDayWithOffset {
param(
[datetime]$DateOnly,
[System.TimeZoneInfo]$Tz
)
# Local 00:00 in the TZ
$local = Get-Date -Date "$($DateOnly.ToString('yyyy-MM-dd')) 00:00:00"
$offset = $Tz.GetUtcOffset($local)
$sign = if ($offset -lt [TimeSpan]::Zero) { '-' } else { '+' }
$hh = [math]::Abs($offset.Hours).ToString('00')
$mm = [math]::Abs($offset.Minutes).ToString('00')
$offStr = "$sign$hh`:$mm"
return "{0}T00:00:00{1}" -f $local.ToString('yyyy-MM-dd'), $offStr
}
# Helper: connect to Graph with appropriate scope
function Connect-GraphWithScope {
param([string]$Scope)
try {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
} catch {}
try {
Connect-MgGraph -Scopes $Scope -ErrorAction Stop | Out-Null
} catch {
Write-Error "Failed to connect to Microsoft Graph with scope '$Scope': $($_.Exception.Message)"
Read-Host "Press Enter to exit"
exit 1
}
}
# Helper: validate date input (YYYY-MM-DD)
function Read-DateStrict {
param([string]$Prompt,[datetime]$Default)
while ($true) {
$in = Read-Host "$Prompt (YYYY-MM-DD) [Enter for $($Default.ToString('yyyy-MM-dd'))]"
if ([string]::IsNullOrWhiteSpace($in)) { return $Default.Date }
if ([datetime]::TryParseExact($in, 'yyyy-MM-dd', $null, 'None', [ref]([datetime]$out))) {
return $out.Date
}
Write-Host "Invalid date. Please use YYYY-MM-DD." -ForegroundColor Yellow
}
}
# Start
Write-Host "=== M365 Mailbox Message Counter ===" -ForegroundColor Cyan
Ensure-GraphModule
# 1) Which mailbox type?
Write-Host "Select mailbox type:" -ForegroundColor Cyan
Write-Host " 1) My mailbox"
Write-Host " 2) Another/shared mailbox (requires delegation with Full Access)"
$mode = Read-Host "Enter 1 or 2"
if ($mode -notin @('1','2')) {
Write-Error "Invalid selection."
Read-Host "Press Enter to exit"
exit 1
}
# 2) Target mailbox SMTP
$Mailbox = Read-Host "Enter the mailbox SMTP address to check (e.g., user@domain.com)"
if ([string]::IsNullOrWhiteSpace($Mailbox) -or ($Mailbox -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$')) {
Write-Error "Invalid email address."
Read-Host "Press Enter to exit"
exit 1
}
# 3) Date range (inclusive start, exclusive end)
# Defaults to January 2025 for convenience; change as needed
$defaultStart = Get-Date '2025-01-01'
$defaultEnd = Get-Date '2025-02-01'
$StartDate = Read-DateStrict -Prompt "Start date (inclusive)" -Default $defaultStart
$EndDate = Read-DateStrict -Prompt "End date (exclusive)" -Default $defaultEnd
if ($EndDate -le $StartDate) {
Write-Error "End date must be AFTER start date."
Read-Host "Press Enter to exit"
exit 1
}
# Prepare TZ and ISO strings with correct offsets for each boundary date
$tz = Get-TimeZoneInfoSafe -Id $TimeZoneId
$startISO = Get-ISOStartOfDayWithOffset -DateOnly $StartDate -Tz $tz
$endISO = Get-ISOStartOfDayWithOffset -DateOnly $EndDate -Tz $tz
# 4) Connect with proper scope
$scope = if ($mode -eq '1') { 'Mail.Read' } else { 'Mail.Read.Shared' }
Connect-GraphWithScope -Scope $scope
# Show who is signed in (helpful for multi-tenant admins)
try {
$ctx = Get-MgContext
if ($ctx) { Write-Host "Connected as: $($ctx.Account) | Tenant: $($ctx.TenantId) | Scope(s): $($ctx.Scopes -join ', ')" -ForegroundColor DarkGray }
} catch {}
# 5) Perform count
try {
$count = 0
# Exclude drafts and self-sent (from == mailbox) to count RECEIVED mail
$filter = "receivedDateTime ge $startISO and receivedDateTime lt $endISO and isDraft eq false and (from/emailAddress/address ne '$Mailbox')"
# Ask Graph for a count; we don't need to page through items (PageSize 1 trick)
Get-MgUserMessage -UserId $Mailbox -Filter $filter -CountVariable count -PageSize 1 | Out-Null
Write-Host ""
Write-Host "Mailbox: $Mailbox" -ForegroundColor Green
Write-Host "Window: $($StartDate.ToString('yyyy-MM-dd')) to $($EndDate.ToString('yyyy-MM-dd')) ($TimeZoneId)" -ForegroundColor Green
Write-Host "Count of RECEIVED messages (all folders, excluding drafts & self-sent):" -ForegroundColor Green
Write-Host $count -ForegroundColor Cyan
} catch {
Write-Host ""
Write-Error "Query failed: $($_.Exception.Message)"
# Common guidance
if ($mode -eq '2') {
Write-Host "If this is another/shared mailbox, ensure:" -ForegroundColor Yellow
Write-Host " - Your account has Full Access to $Mailbox in Exchange Online (Mailbox Delegation)." -ForegroundColor Yellow
} else {
Write-Host "If this is your own mailbox and it still fails, try reconnecting and consenting the scope." -ForegroundColor Yellow
}
}
Write-Host ""
Read-Host "Press Enter to exit"