<# .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"