PowerShell Script: Automated Daily Disk Space Report by Email

Disk-full events on production servers are almost always preventable — they are never sudden, just unmonitored. Windows built-in disk alerts require manual configuration per server and generate event log entries that nobody reads at 2 AM. A PowerShell script that polls every server, calculates free-space percentages, color-codes critical volumes, and emails a formatted HTML table takes an afternoon to build and then saves you from a great number of midnight calls. This guide builds that script from data collection through to email delivery via the Graph API.
Collecting Drive Data with Get-PSDrive and Get-CimInstance
Get-PSDrive is convenient for local drives but it only returns drives visible in the current session and does not work against remote computers. For multi-server collection, use Get-CimInstance -ClassName Win32_LogicalDisk, which returns fixed drives (DriveType 3) with size and free-space in bytes from any machine you can reach over WinRM or DCOM.
# Local quick check — useful for testing threshold logic
Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -gt 0 } |
Select-Object Name,
@{N='SizeGB'; E={[math]::Round($_.Used/1GB + $_.Free/1GB, 1)}},
@{N='FreeGB'; E={[math]::Round($_.Free/1GB, 1)}},
@{N='FreePct'; E={[math]::Round($_.Free / ($_.Used + $_.Free) * 100, 1)}}
Name SizeGB FreeGB FreePct
---- ------ ------ -------
C 237.5 88.2 37.1
D 500.0 341.6 68.3
Running Against Multiple Servers with Invoke-Command
Wrap the CIM query in Invoke-Command to fan out across a server list in parallel. The -ThrottleLimit parameter caps concurrent sessions to avoid overwhelming WinRM. Collect results using $using: to pass threshold values into the remote scriptblock.
$servers = @('SRV-APP01','SRV-DB01','SRV-FILE01','SRV-WEB01')
$warnThreshold = 20 # % free — yellow
$critThreshold = 10 # % free — red
$diskData = Invoke-Command -ComputerName $servers -ThrottleLimit 10 -ScriptBlock {
$warn = $using:warnThreshold
$crit = $using:critThreshold
Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction Stop |
Select-Object @{N='Server'; E={$env:COMPUTERNAME}},
DeviceID,
@{N='SizeGB'; E={[math]::Round($_.Size/1GB, 1)}},
@{N='FreeGB'; E={[math]::Round($_.FreeSpace/1GB, 1)}},
@{N='FreePct'; E={[math]::Round($_.FreeSpace / $_.Size * 100, 1)}},
@{N='Status'; E={
$pct = [math]::Round($_.FreeSpace / $_.Size * 100, 1)
if ($pct -le $crit) { 'Critical' }
elseif ($pct -le $warn) { 'Warning' }
else { 'OK' }
}}
} -ErrorAction SilentlyContinue
Setting Warning and Critical Thresholds
Define thresholds as parameters at the top of the script so they can be overridden when calling from a scheduled task or a monitoring orchestrator. Consider different thresholds for different drive types — a system drive at 10 % free is more critical than a log archive volume at the same percentage. A simple approach is to keep two variables ($warnThreshold and $critThreshold) as percentages and derive the status in the CIM query as shown above.
Building an HTML Table with Color-Coded Rows
PowerShell’s ConvertTo-Html produces plain HTML without row-level styling. Build the table manually using a StringBuilder or a here-string loop so you can apply a CSS class per row based on drive status.
$style = @"
<style>
body { font-family: Segoe UI, sans-serif; font-size: 13px; }
table { border-collapse: collapse; width: 100%; }
th { background: #1e3a5f; color: #fff; padding: 6px 12px; text-align: left; }
td { padding: 5px 12px; border-bottom: 1px solid #e0e0e0; }
.Warning { background: #fff3cd; }
.Critical { background: #f8d7da; font-weight: bold; }
.OK { background: #d4edda; }
</style>
"@
$rows = $diskData | Sort-Object Server, DeviceID | ForEach-Object {
"<tr class='$($_.Status)'><td>$($_.Server)</td><td>$($_.DeviceID)</td>" +
"<td>$($_.SizeGB) GB</td><td>$($_.FreeGB) GB</td>" +
"<td>$($_.FreePct) %</td><td>$($_.Status)</td></tr>"
}
$table = "<table><tr><th>Server</th><th>Drive</th><th>Size</th><th>Free</th><th>Free %</th><th>Status</th></tr>$($rows -join '')`</table>"
$body = "$style<h2>Disk Space Report — $(Get-Date -Format 'yyyy-MM-dd HH:mm')</h2>$table"
Sending the Report via Graph API Mail Endpoint
Send-MailMessage is marked deprecated and relies on Basic Auth SMTP, which Microsoft 365 is progressively disabling. The modern replacement is the Graph API sendMail endpoint, called with an app registration that has Mail.Send permission. The body is a JSON payload with the HTML content embedded as a string.
$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$tokenBody = @{
client_id = $appId
client_secret = $appSecret
scope = 'https://graph.microsoft.com/.default'
grant_type = 'client_credentials'
}
$token = (Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $tokenBody).access_token
$mailPayload = @{
message = @{
subject = "Disk Space Report $(Get-Date -Format 'yyyy-MM-dd')"
body = @{ contentType = 'HTML'; content = $body }
toRecipients = @(@{ emailAddress = @{ address = '[email protected]' } })
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$senderUpn/sendMail" `
-Method Post `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType 'application/json' `
-Body $mailPayload
Scheduling with Windows Task Scheduler
Register the script as a scheduled task to run daily at 07:00. Use the -RunLevel Highest option so the task runs elevated and can reach all WinRM targets. Store credentials in a service account rather than your personal account so the task survives password changes.
Common Errors
- Send-MailMessage is deprecated — switching to Graph API send-mail requires app registration. Create an app registration in Entra ID, grant
Mail.Sendapplication permission with admin consent, and generate a client secret or certificate. The secret must be stored securely — use a Key Vault reference in the scheduled task or a Windows Credential Manager entry, never a plain-text value in the script. - Get-PSDrive shows only local drives — CIM Win32_LogicalDisk needed for remote hosts.
Get-PSDrive -PSProvider FileSystemenumerates drives mounted in the current session only. It cannot query remote machines. Always useGet-CimInstance Win32_LogicalDiskinside anInvoke-Commandblock for multi-server collection.
Related Cmdlets / See Also
- PowerShell Disk Management Commands
- Creating HTML Reports with PowerShell
- Scheduling PowerShell Scripts with Task Scheduler
Wrapping Up
A daily disk space report takes one afternoon to build and runs forever without attention. Use Get-CimInstance inside Invoke-Command for multi-server data, build color-coded HTML rows manually for status clarity, and send via the Graph API to stay compliant with modern M365 authentication requirements.


