196 lines
7.7 KiB
PowerShell
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"
|