Proactively monitoring server storage is a critical task for any system administrator. When running Windows Server instances within Oracle Cloud Infrastructure (OCI), unexpected disk utilization can lead to application downtime and database corruption. This step-by-step guide explains how to deploy and configure the Send-DiskSpaceAlert.ps1 PowerShell script on a Windows Server hosted in OCI. The script monitors free disk space and sends polished HTML email alerts using the OCI Email Delivery SMTP service when drives fall below configured thresholds.
Prerequisites
- Windows Server 2016 / 2019 / 2022 / 2025 hosted in OCI
- PowerShell 5.1 or later (included in all supported Windows Server versions)
- An active OCI Email Delivery configuration (see Configuring Oracle Cloud Email Delivery Service)
- Administrator access on the server
Step 1 — Copy the Script
Create a folder for the script and place Send-DiskSpaceAlert.ps1 inside it:
C:\script\Send-DiskSpaceAlert.ps1
The script will also write two files to this same folder automatically:
DiskAlertLog.txt— rolling log of every check and alertsmtp_cred.xml— encrypted SMTP credential (created in Step 3)
Step 2 — Edit the CONFIG Block
Open the script in a text editor and update the $config section near the top with your OCI Email Delivery details:
# =============================================================================
# Send-DiskSpaceAlert.ps1
# Monitors disk free space and sends email alert via OCI Email Delivery
#
# SETUP:
# 1. Edit the CONFIG section below
# 2. Save SMTP password: .\Send-DiskSpaceAlert.ps1 -SaveCredential
# 3. Test email: .\Send-DiskSpaceAlert.ps1 -Test
# 4. Schedule it: .\Send-DiskSpaceAlert.ps1 -CreateScheduledTask
# =============================================================================
[CmdletBinding()]
param(
[switch]$SaveCredential,
[switch]$Test,
[switch]$CreateScheduledTask
)
# =============================================================================
# CONFIG - Edit these values
# =============================================================================
$config = @{
WarningThresholdGB = 20
CriticalThresholdGB = 10
DrivesToMonitor = @() # Empty = all drives. Or specify e.g. @("C","D")
SmtpServer = "smtp.email.us-ashburn-1.oci.oraclecloud.com" # Replace with your region endpoint
SmtpPort = 587
UseSsl = $true
FromAddress = "alerts@yourdomain.com" # Must match an OCI Approved Sender
ToAddresses = @("admin@yourdomain.com")
EmailSubjectPrefix = "[OCI-DiskAlert]"
SmtpUsername = "ocid1.user.oc1..your-smtp-user@yourtenant.com"
CredentialFile = "$PSScriptRoot\smtp_cred.xml"
LogFile = "$PSScriptRoot\DiskAlertLog.txt"
MaxLogSizeMB = 5
TaskName = "DiskSpaceAlert"
TaskIntervalMinutes = 30
}
# =============================================================================
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$timestamp] [$Level] $Message"
Write-Host $line
Add-Content -Path $config.LogFile -Value $line -ErrorAction SilentlyContinue
if (Test-Path $config.LogFile) {
$logSizeMB = (Get-Item $config.LogFile).Length / 1MB
if ($logSizeMB -gt $config.MaxLogSizeMB) {
$lines = Get-Content $config.LogFile
$trimmed = $lines | Select-Object -Last 500
$trimmed | Set-Content $config.LogFile
Write-Log "Log trimmed to last 500 lines"
}
}
}
function Save-SmtpCredential {
Write-Host "Enter OCI SMTP credentials to save to: $($config.CredentialFile)"
$cred = Get-Credential -UserName $config.SmtpUsername -Message "Enter OCI SMTP password"
$cred | Export-Clixml -Path $config.CredentialFile
Write-Host "Credential saved." -ForegroundColor Green
Write-Host "NOTE: Encrypted to the current Windows user/machine only." -ForegroundColor Yellow
}
function Get-SmtpCredential {
if ([string]::IsNullOrWhiteSpace($config.SmtpUsername)) { return $null }
if (-not (Test-Path $config.CredentialFile)) {
Write-Log "Credential file not found: $($config.CredentialFile). Run with -SaveCredential first." "ERROR"
return $null
}
try {
return Import-Clixml -Path $config.CredentialFile
} catch {
Write-Log "Failed to load credential: $_" "ERROR"
return $null
}
}
function Send-AlertEmail {
param(
[string]$Subject,
[string]$Body,
[string]$Priority = "Normal"
)
$mailParams = @{
SmtpServer = $config.SmtpServer
Port = $config.SmtpPort
UseSsl = $config.UseSsl
From = $config.FromAddress
To = $config.ToAddresses
Subject = "$($config.EmailSubjectPrefix) $Subject"
Body = $Body
BodyAsHtml = $true
Priority = $Priority
ErrorAction = "Stop"
}
$cred = Get-SmtpCredential
if ($cred) { $mailParams.Credential = $cred }
try {
Send-MailMessage @mailParams
Write-Log "Email sent: $Subject"
return $true
} catch {
Write-Log "Failed to send email: $_" "ERROR"
return $false
}
}
function Get-DiskInfo {
$drives = Get-PSDrive -PSProvider FileSystem | Where-Object {
$_.Used -ne $null -and $_.Free -ne $null -and ($_.Used + $_.Free) -gt 0
}
if ($config.DrivesToMonitor.Count -gt 0) {
$drives = $drives | Where-Object { $config.DrivesToMonitor -contains $_.Name }
}
return $drives | ForEach-Object {
$totalGB = [math]::Round(($_.Used + $_.Free) / 1GB, 2)
$freeGB = [math]::Round($_.Free / 1GB, 2)
$usedGB = [math]::Round($_.Used / 1GB, 2)
$freePct = if ($totalGB -gt 0) { [math]::Round(($freeGB / $totalGB) * 100, 1) } else { 0 }
[PSCustomObject]@{
Drive = $_.Name
TotalGB = $totalGB
FreeGB = $freeGB
UsedGB = $usedGB
FreePercent = $freePct
}
}
}
function Build-EmailBody {
param(
[array]$AlertDrives,
[array]$AllDrives,
[string]$AlertLevel
)
$headerColor = if ($AlertLevel -eq "CRITICAL") { "#c0392b" } else { "#e67e22" }
$hostname = $env:COMPUTERNAME
$timestamp = Get-Date -Format "dddd, MMMM dd yyyy HH:mm:ss"
$warnGB = $config.WarningThresholdGB
$critGB = $config.CriticalThresholdGB
# Build alert list items using string concatenation (avoids PowerShell parsing HTML)
$alertItems = ""
foreach ($d in $AlertDrives) {
$alertItems += '<li>Drive <strong>' + $d.Drive + ':\</strong> - <strong>' + $d.FreeGB + ' GB</strong> free (' + $d.FreePercent + '%)</li>'
}
# Build drive table rows
$driveRows = ""
foreach ($d in $AllDrives) {
$usedPct = [math]::Round(($d.UsedGB / $d.TotalGB) * 100, 1)
$freeColor = if ($d.FreeGB -lt $critGB) { "#c0392b" }
elseif ($d.FreeGB -lt $warnGB) { "#e67e22" }
else { "#27ae60" }
$barWidth = [math]::Min($usedPct, 100)
$bar = '<div style="background:#e0e0e0;border-radius:4px;height:10px;width:200px;display:inline-block;vertical-align:middle;">'
$bar += '<div style="background:' + $freeColor + ';width:' + $barWidth + '%;height:10px;border-radius:4px;"></div></div>'
$driveRows += '<tr style="border-bottom:1px solid #eee;">'
$driveRows += '<td style="padding:8px 12px;font-weight:bold;">' + $d.Drive + ':\</td>'
$driveRows += '<td style="padding:8px 12px;">' + $d.TotalGB + ' GB</td>'
$driveRows += '<td style="padding:8px 12px;color:' + $freeColor + ';font-weight:bold;">' + $d.FreeGB + ' GB</td>'
$driveRows += '<td style="padding:8px 12px;">' + $usedPct + '%</td>'
$driveRows += '<td style="padding:8px 12px;">' + $bar + '</td>'
$driveRows += '</tr>'
}
# Assemble full HTML body
$html = '<!DOCTYPE html><html><body style="font-family:Segoe UI,Arial,sans-serif;background:#f5f5f5;margin:0;padding:20px;">'
$html += '<div style="max-width:620px;margin:0 auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1);">'
# Header banner
$html += '<div style="background:' + $headerColor + ';padding:20px 24px;">'
$html += '<h2 style="color:#fff;margin:0;font-size:20px;">[!] Disk Space ' + $AlertLevel + '</h2>'
$html += '<p style="color:rgba(255,255,255,0.85);margin:4px 0 0;font-size:13px;">' + $hostname + ' - ' + $timestamp + '</p>'
$html += '</div>'
# Alert summary
$html += '<div style="padding:20px 24px;border-bottom:1px solid #eee;">'
$html += '<p style="margin:0 0 8px;font-size:14px;color:#333;">The following drive(s) have fallen below the threshold:</p>'
$html += '<ul style="margin:0;padding-left:20px;font-size:14px;color:#555;">' + $alertItems + '</ul>'
$html += '<p style="margin:12px 0 0;font-size:13px;color:#888;">Thresholds - Warning: <strong>' + $warnGB + ' GB</strong> | Critical: <strong>' + $critGB + ' GB</strong></p>'
$html += '</div>'
# All drives table
$html += '<div style="padding:20px 24px;">'
$html += '<p style="margin:0 0 12px;font-size:14px;font-weight:bold;color:#333;">All Monitored Drives</p>'
$html += '<table style="width:100%;border-collapse:collapse;font-size:13px;color:#444;">'
$html += '<thead><tr style="background:#f8f8f8;text-align:left;">'
$html += '<th style="padding:8px 12px;">Drive</th>'
$html += '<th style="padding:8px 12px;">Total</th>'
$html += '<th style="padding:8px 12px;">Free</th>'
$html += '<th style="padding:8px 12px;">Used %</th>'
$html += '<th style="padding:8px 12px;">Usage</th>'
$html += '</tr></thead>'
$html += '<tbody>' + $driveRows + '</tbody></table></div>'
# Footer
$html += '<div style="padding:12px 24px;background:#f8f8f8;border-top:1px solid #eee;">'
$html += '<p style="margin:0;font-size:11px;color:#aaa;">Sent via OCI Email Delivery by Send-DiskSpaceAlert.ps1 on ' + $hostname + '</p>'
$html += '</div></div></body></html>'
return $html
}
function Register-AlertTask {
$scriptPath = $PSCommandPath
$argString = '-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + $scriptPath + '"'
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $argString
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes $config.TaskIntervalMinutes) -Once -At (Get-Date)
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5) -RestartCount 2 -RestartInterval (New-TimeSpan -Minutes 1)
try {
Register-ScheduledTask -TaskName $config.TaskName `
-Action $action -Trigger $trigger -Principal $principal -Settings $settings `
-Description "Monitors disk free space and sends email alerts via OCI Email Delivery" -Force | Out-Null
Write-Host "Scheduled task '$($config.TaskName)' created - runs every $($config.TaskIntervalMinutes) min as SYSTEM." -ForegroundColor Green
Write-Host "Remember: save credentials via PsExec as SYSTEM so the task can decrypt the password." -ForegroundColor Yellow
} catch {
Write-Log "Failed to create scheduled task: $_" "ERROR"
}
}
# =============================================================================
# MAIN
# =============================================================================
if ($SaveCredential) { Save-SmtpCredential; exit 0 }
if ($CreateScheduledTask) { Register-AlertTask; exit 0 }
Write-Log "Starting disk space check on $env:COMPUTERNAME"
$allDrives = Get-DiskInfo
$criticalDrives = $allDrives | Where-Object { $_.FreeGB -lt $config.CriticalThresholdGB }
$warningDrives = $allDrives | Where-Object { $_.FreeGB -lt $config.WarningThresholdGB -and $_.FreeGB -ge $config.CriticalThresholdGB }
foreach ($d in $allDrives) {
Write-Log "Drive $($d.Drive):\ - Free: $($d.FreeGB) GB / $($d.TotalGB) GB ($($d.FreePercent)% free)"
}
if ($Test) {
Write-Log "TEST MODE - sending test email via OCI Email Delivery"
$body = Build-EmailBody -AlertDrives $allDrives -AllDrives $allDrives -AlertLevel "WARNING"
Send-AlertEmail -Subject "TEST - Disk Space Check on $env:COMPUTERNAME" -Body $body
exit 0
}
if ($criticalDrives) {
Write-Log "CRITICAL: $($criticalDrives.Count) drive(s) below $($config.CriticalThresholdGB) GB" "ERROR"
$body = Build-EmailBody -AlertDrives $criticalDrives -AllDrives $allDrives -AlertLevel "CRITICAL"
Send-AlertEmail -Subject "CRITICAL - Low Disk Space on $env:COMPUTERNAME" -Body $body -Priority "High"
}
elseif ($warningDrives) {
Write-Log "WARNING: $($warningDrives.Count) drive(s) below $($config.WarningThresholdGB) GB" "WARN"
$body = Build-EmailBody -AlertDrives $warningDrives -AllDrives $allDrives -AlertLevel "WARNING"
Send-AlertEmail -Subject "WARNING - Low Disk Space on $env:COMPUTERNAME" -Body $body
}
else {
Write-Log "All drives OK. No alert sent."
}
Write-Log "Disk space check complete."
Step 3 — Save the SMTP Credential Securely
The script stores the OCI SMTP password encrypted on disk using Windows DPAPI (Data Protection API) via Export-Clixml. The encrypted file can only be decrypted by the same Windows user account that saved it.
Why this matters
The scheduled task (Step 5) runs as the SYSTEM account. If you save the credential as your own user, SYSTEM cannot decrypt it and the task will fail silently. You must save the credential while running as SYSTEM.
How to save the credential as SYSTEM using PsExec
1. Download PsExec (part of Microsoft Sysinternals):
Invoke-WebRequest -Uri "https://live.sysinternals.com/PsExec64.exe" -OutFile "C:\script\PsExec64.exe"
2. Open a SYSTEM-context PowerShell window (run this from an elevated prompt):
C:\script\PsExec64.exe -s -i powershell.exe
A new PowerShell window opens. Verify it is running as SYSTEM:
whoami
# Expected output: nt authority\system
3. Run the save command from that SYSTEM PowerShell window:
C:\script\Send-DiskSpaceAlert.ps1 -SaveCredential
Enter your OCI SMTP username and generated password when prompted. The credential is saved to smtp_cred.xml encrypted under the SYSTEM account.
4. Close the PsExec window.
Step 4 — Send a Test Email
Before scheduling, verify the script works end-to-end:
# Run from a normal elevated PowerShell
C:\script\Send-DiskSpaceAlert.ps1 -Test
This sends a test email immediately regardless of actual disk usage. Check your inbox and confirm the HTML table renders correctly with drive information.
If the email does not arrive, check C:\script\DiskAlertLog.txt for error details (such as OCI egress security list blocks or unapproved sender errors).
Step 5 — Create the Scheduled Task
Register the script as a Windows Scheduled Task that runs every 30 minutes as SYSTEM:
# Must be run from an elevated (Administrator) PowerShell
C:\script\Send-DiskSpaceAlert.ps1 -CreateScheduledTask
To verify it was created:
- Open Task Scheduler (
taskschd.msc) - Look for DiskSpaceAlert under Task Scheduler Library
- Right-click → Run to trigger it immediately
- Check
DiskAlertLog.txtto confirm it executed successfully as SYSTEM
Alert Behaviour
The script sends two levels of alert:
| Level | Condition | Email Priority |
|---|---|---|
| WARNING | Free space below WarningThresholdGB (default 20 GB) |
Normal |
| CRITICAL | Free space below CriticalThresholdGB (default 10 GB) |
High |
Each email includes a colour-coded HTML table showing all monitored drives with their total size, free space, used percentage, and a visual usage bar.
No email is sent if all drives are above the warning threshold — the result is only written to the log file.
Changing the Check Interval
The default interval is every 30 minutes. To change it, edit TaskIntervalMinutes in the CONFIG block before running -CreateScheduledTask:
TaskIntervalMinutes = 15 # Check every 15 minutes
If the task already exists, delete it from Task Scheduler and re-run -CreateScheduledTask.
Log File
The script maintains a rolling log at C:\script\DiskAlertLog.txt. It is automatically trimmed to the last 500 lines when it exceeds 5 MB (configurable via MaxLogSizeMB).
Example log output:
[2026-05-12 08:30:01] [INFO] Starting disk space check on SERVER01
[2026-05-12 08:30:01] [INFO] Drive C:\ - Free: 8.42 GB / 127.00 GB (6.6% free)
[2026-05-12 08:30:01] [ERROR] CRITICAL: 1 drive(s) below 10 GB
[2026-05-12 08:30:03] [INFO] Email sent: CRITICAL - Low Disk Space on SERVER01
