PowerShell scripts that send individual email notifications for Active Directory user accounts with an approaching account expiration date. Includes an encrypted SMTP credential helper and a Scheduled Task installer for automated recurring execution.
| File | Description |
|---|---|
Get-ExpiringADAccounts.ps1 |
Main script — queries Active Directory and sends per-account email notifications |
Save-SmtpCredential.ps1 |
Saves SMTP credentials to a DPAPI-encrypted file for authenticated sending |
Install-ScheduledTask.ps1 |
Registers or removes a Windows Scheduled Task for the main script |
- Windows PowerShell 5.1 or PowerShell 7+
- RSAT: Active Directory module installed, or run directly on a domain controller
- Read access to Active Directory
- Administrator privileges (required for the Scheduled Task installer only)
- Access to an SMTP server
# 1. (Optional) Save SMTP credentials for authenticated sending.
# Run as the same user that will execute the main script.
.\Save-SmtpCredential.ps1
# 2. Run the notification script.
.\Get-ExpiringADAccounts.ps1 -SmtpServer "smtp.example.com" -Recipient "tickets@example.com"
# 3. (Optional) Install a Scheduled Task for daily execution (run as Administrator).
.\Install-ScheduledTask.ps1 -Action Install -SmtpServer "smtp.example.com" -Recipient "tickets@example.com"Prompts for an SMTP username and password and stores them in a DPAPI-encrypted file (smtp-credential.xml) next to the script. The main script loads this file automatically to authenticate with the SMTP server.
- DPAPI encryption ties the file to the current user and machine. It must be created by the same account (and on the same machine) that runs
Get-ExpiringADAccounts.ps1. - If the main script runs under a different identity than the one that saved the file, decryption fails and authentication is skipped.
# Save credentials for the interactive user
.\Save-SmtpCredential.ps1
# Save credentials for the SYSTEM account (so a SYSTEM-run Scheduled Task can use them)
psexec -s -i powershell -File "C:\Dev\Powershell\AD-expiring-accounts\Save-SmtpCredential.ps1"If no credential file is present, the main script sends email without authentication and emits a warning.
| Parameter | Type | Default | Description |
|---|---|---|---|
-DaysUntilExpiration |
int | 30 |
Upper boundary: accounts expiring within this many days are included |
-SmtpServer |
string | (required) | SMTP server hostname or IP |
-SmtpPort |
int | 25 |
SMTP server port |
-Sender |
string | (optional) | FROM email address. If omitted and a credential file is loaded, the credential username is used as the sender |
-Recipient |
string | (required) | TO email address (ticketing system) |
-UseSsl |
switch | — | Enable SSL for SMTP connection |
-CredentialFilePath |
string | (auto) | Path to the DPAPI-encrypted credential file. Defaults to smtp-credential.xml in the script directory |
-StateFilePath |
string | (auto) | Path to the JSON state file tracking sent notifications. Defaults to SentNotifications.json in the script directory |
- Only enabled accounts are included.
- Only accounts with
AccountExpirationDatebetween today and today + DaysUntilExpiration are included. - Accounts with no expiration date are automatically excluded.
For each expiring account not yet notified, the script sends an email with:
- Subject:
AD Account Expiring: <UserPrincipalName> (<ExpirationDate>) - Body: Plain text with account and manager details
- Attachments: Single-row CSV and XML files for the account
| Field | Description |
|---|---|
| Login Name | User logon name (SamAccountName) |
| First Name | Given name |
| Last Name | Surname |
| Email address | |
| Company | Company attribute |
| Expires | Expiration date (yyyy-MM-dd) |
| Days Left | Days until expiration (0 = expires today) |
| Manager — Login Name | Manager's logon name |
| Manager — First Name | Manager's first name |
| Manager — Last Name | Manager's last name |
| Manager — Email | Manager's email address |
Manager fields are empty if no manager is assigned or the manager object cannot be resolved.
The attached exports contain the same fields as the body plus UserPrincipalName: SamAccountName, UserPrincipalName, GivenName, Surname, Mail, Company, AccountExpirationDate, DaysRemaining, ManagerSamAccountName, ManagerGivenName, ManagerSurname, ManagerMail.
The script maintains a JSON state file (SentNotifications.json by default) that tracks which SamAccountName + AccountExpirationDate combinations have already been notified. This prevents duplicate notifications. A unique key is formed from the account name and expiration date, so the same user with a changed expiration date will still receive a new notification.
# Send with defaults (30 days, port 25, no SSL); sender taken from saved credential
.\Get-ExpiringADAccounts.ps1 -SmtpServer "smtp.example.com" -Recipient "tickets@example.com"
# Explicit sender address
.\Get-ExpiringADAccounts.ps1 -SmtpServer "smtp.example.com" -Sender "ad-alerts@example.com" -Recipient "tickets@example.com"
# 60-day window with SSL on port 587
.\Get-ExpiringADAccounts.ps1 -SmtpServer "smtp.example.com" -SmtpPort 587 -Recipient "tickets@example.com" -UseSsl -DaysUntilExpiration 60
# Custom credential and state file locations
.\Get-ExpiringADAccounts.ps1 -SmtpServer "smtp.example.com" -Recipient "tickets@example.com" `
-CredentialFilePath "C:\Secure\smtp-credential.xml" `
-StateFilePath "C:\State\SentNotifications.json"
# Verbose output with progress details
.\Get-ExpiringADAccounts.ps1 -SmtpServer "smtp.example.com" -Recipient "tickets@example.com" -VerboseAfter processing, the script prints a summary to the console:
========================================
Notification Summary
========================================
Window : Today to +30 days
Total found : 12
Notified : 8
Already sent : 4
State file : C:\Dev\Powershell\AD-expiring-accounts\SentNotifications.json
========================================
Registers (or removes) a Windows Scheduled Task that runs Get-ExpiringADAccounts.ps1 on a recurring schedule. Must be run as Administrator.
The task does not pass a credential path; the main script falls back to its default
smtp-credential.xmlnext to the script. Make sure the credential file is saved by the same account the task runs as (see Save-SmtpCredential.ps1).
| Parameter | Type | Default | Description |
|---|---|---|---|
-Action |
string | Install |
Install or Uninstall |
-ScriptPath |
string | (same folder) | Path to Get-ExpiringADAccounts.ps1. Defaults to same directory as installer |
-DaysUntilExpiration |
int | 30 |
Days window passed to the main script |
-SmtpServer |
string | (required) | SMTP server hostname or IP |
-SmtpPort |
int | 25 |
SMTP server port |
-Sender |
string | (optional) | Sender email address (falls back to credential username if omitted) |
-Recipient |
string | (required) | Recipient email address |
-UseSsl |
switch | — | Enable SSL for SMTP connection |
-TaskName |
string | AD-ExpiringAccounts |
Name of the Scheduled Task |
-TaskDescription |
string | (auto) | Description shown in Task Scheduler |
-RunAsUser |
string | SYSTEM |
Account to run the task as. Use SYSTEM or DOMAIN\ServiceAccount |
-RunAsPassword |
string | Password for -RunAsUser (not required for SYSTEM) |
|
-TriggerTime |
string | 07:00 |
Daily run time in HH:mm format |
-TriggerDaysInterval |
int | 1 |
Run every N days |
# Install with required SMTP params
.\Install-ScheduledTask.ps1 -Action Install -SmtpServer "smtp.example.com" -Recipient "tickets@example.com"
# Install with SSL, custom schedule, and service account
.\Install-ScheduledTask.ps1 -Action Install `
-SmtpServer "smtp.example.com" `
-SmtpPort 587 `
-Sender "ad-alerts@example.com" `
-Recipient "tickets@example.com" `
-UseSsl `
-RunAsUser "DOMAIN\svc_reports" `
-RunAsPassword "P@ssw0rd!" `
-TriggerTime "06:30" `
-DaysUntilExpiration 60
# Preview without making changes
.\Install-ScheduledTask.ps1 -Action Install -SmtpServer "smtp.example.com" -Recipient "tickets@example.com" -WhatIf
# Run the task immediately after installing
Start-ScheduledTask -TaskName "AD-ExpiringAccounts"
# Uninstall the task
.\Install-ScheduledTask.ps1 -Action Uninstall
# Uninstall a custom-named task
.\Install-ScheduledTask.ps1 -Action Uninstall -TaskName "MyCustomTaskName"The registered task is configured with:
- Execution time limit: 1 hour
- Multiple instances: Ignore new (prevents overlap)
- Start when available: Yes (catches up if the machine was off at trigger time)
- Run only if network available: Yes
- Run level: Highest (elevated)
- The LDAP query runs against the domain controller directly using file-time integer comparisons, so only matching accounts are transferred — no full user enumeration.
- Exports use UTF-8 encoding.
- The state file is created automatically if it does not exist.
- The credential file is per-user and per-machine (DPAPI); recreate it if you change the account or move machines.
- Get built-in help with
Get-Help .\Get-ExpiringADAccounts.ps1 -Full.