Shore Up
A calendar page with several dates circled, each with a small envelope flying out toward a person, while a clock ticks down in the corner.
Windows

Automate Expiring AD Password Notifications with PowerShell

Ketan Aagja9 min read
No ratings yet

Before you run this

This script reads every enabled AD user's password-expiry date, works out who expires within a window you set (say, the next 14 days), and emails each of those users a reminder. It reads Active Directory and sends mail — it does not change a single account, reset a password, or alter a policy. That makes it low-risk, but it can still misfire loudly: point it at the whole domain with a bad window and you can email hundreds of people at once, so treat the first live run as the dangerous part.

Privileges: reading these attributes does not need Domain Admin. Any authenticated domain user can read msDS-UserPasswordExpiryTimeComputed and mail. You do need the ActiveDirectory PowerShell module (part of RSAT — installed by default on a domain controller, added via RSAT on a member server or workstation). Creating the scheduled task that runs it does need local Administrator on the box you schedule it on. Run the script itself under a plain service or domain account, not an admin.

Test first — this is not optional. Read the script before running it. Run it in report-only mode (the -ReportOnly switch below) first, on your own workstation, so it writes the list of who would be mailed to the console and a CSV instead of sending anything. Confirm the names, the dates, and the addresses look right. Only after that do you let it send. Nothing here is destructive and nothing needs undoing except the scheduled task, which you can disable or delete at any time.

Assumptions. I'm writing for Windows Server 2019/2022 AD, Windows PowerShell 5.1 (the version that ships in-box), an on-prem domain with an internal SMTP relay that accepts mail from this host, and users who have the mail attribute populated. If you're Azure AD / Entra-only, this doesn't apply — password expiry there is handled differently.

How AD stores the expiry date

Don't compute expiry yourself from PasswordLastSet plus the domain max-age. That ignores fine-grained password policies and gets edge cases wrong. AD exposes a constructed attribute, msDS-UserPasswordExpiryTimeComputed, that already accounts for the policy applied to each user. It's a FILETIME integer you convert with [datetime]::FromFileTime().

Two values need filtering out:

  • Accounts with password-never-expires return 9223372036854775807 (Int64 max). FromFileTime on that throws or returns a nonsense date — skip them.
  • A value of 0 means the password must change at next logon — decide separately whether you want to nag those.

The script

Save this as Notify-ExpiringPasswords.ps1. Replace the obvious placeholders — mail.example.com, it-helpdesk@example.com, and the OU=Staff,DC=example,DC=com search base — with your own.

[CmdletBinding()]
param(
    [int]$WarnDays   = 14,                          # notify when expiry is within this many days
    [string]$SmtpServer = "mail.example.com",       # your internal relay
    [string]$From    = "it-helpdesk@example.com",
    [string]$SearchBase = "OU=Staff,DC=example,DC=com",  # scope; narrow this for testing
    [switch]$ReportOnly                              # when set: list only, send nothing
)

Import-Module ActiveDirectory -ErrorAction Stop

$now       = Get-Date
$cutoff    = $now.AddDays($WarnDays)
$neverExp  = [Int64]::MaxValue        # password-never-expires sentinel value
$report    = @()

# Only enabled users whose password can expire; -Properties pulls the attributes we read
$users = Get-ADUser -SearchBase $SearchBase -Filter {
    Enabled -eq $true -and PasswordNeverExpires -eq $false
} -Properties "msDS-UserPasswordExpiryTimeComputed", mail, GivenName

foreach ($u in $users) {
    $raw = $u."msDS-UserPasswordExpiryTimeComputed"
    if ($null -eq $raw -or $raw -eq 0 -or $raw -eq $neverExp) { continue }

    $expiry   = [datetime]::FromFileTime($raw)
    $daysLeft = ($expiry - $now).Days

    # inside the warning window and not already expired
    if ($expiry -le $cutoff -and $expiry -gt $now) {

        if (-not $u.mail) {
            Write-Warning "$($u.SamAccountName) expires $expiry but has no mail attribute — skipped"
            continue
        }

        $report += [pscustomobject]@{
            Name      = $u.Name
            User      = $u.SamAccountName
            Mail      = $u.mail
            Expires   = $expiry
            DaysLeft  = $daysLeft
        }

        if (-not $ReportOnly) {
            $subject = "Your password expires in $daysLeft day(s)"
            $body = @"
Hi $($u.GivenName),

Your Windows account password will expire on $($expiry.ToString('dddd, dd MMMM yyyy')) — that's $daysLeft day(s) from now.

Please change it before then, while connected to the office network or VPN, by pressing Ctrl+Alt+Del and choosing "Change a password".

If you need help, contact the IT helpdesk.
"@
            Send-MailMessage -To $u.mail -From $From -Subject $subject `
                -Body $body -SmtpServer $SmtpServer -Encoding UTF8
        }
    }
}

# Always output the list so you can see (or log) who was in scope
$report | Sort-Object DaysLeft | Format-Table -AutoSize
$report | Export-Csv -Path "C:\path\to\expiring-passwords.csv" -NoTypeInformation -Encoding UTF8

A note on Send-MailMessage: Microsoft has marked it obsolete and no longer improves it, but it still functions in Windows PowerShell 5.1 and is the standard in-box way to send a quick internal notification. If you'd rather not depend on it, the common alternatives are your own SMTP call via System.Net.Mail.SmtpClient, or a mail-relay module — I'm not walking through those here.

Test run

Start narrow. Point -SearchBase at a test OU, or at your own account, and use -ReportOnly so nothing is sent:

.\Notify-ExpiringPasswords.ps1 -WarnDays 14 -ReportOnly -SearchBase "OU=TestUsers,DC=example,DC=com"

You'll get a table and a CSV of exactly who would be mailed. Check the expiry dates against a user you know (see the verification section). Once that looks right, drop -ReportOnly and re-run against the same small OU so one or two real people get a real mail you can inspect. Only then widen the search base.

Schedule it

Once you trust it, run it daily with Task Scheduler. Create the task under an account that can read AD and reach the relay (a dedicated service account is cleaner than a personal one):

$action  = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\path\to\Notify-ExpiringPasswords.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At 7am

Register-ScheduledTask -TaskName "Notify Expiring AD Passwords" `
    -Action $action -Trigger $trigger -Description "Emails users whose AD password expires soon"

Register-ScheduledTask will prompt for the run-as account, or you can supply it with the -User and -Password parameters — check Microsoft Learn for Register-ScheduledTask for the exact credential parameters if you script the account in.

Verify it worked

Cross-check one user's date against what AD actually holds, so you trust the maths:

$u = Get-ADUser "someuser" -Properties "msDS-UserPasswordExpiryTimeComputed"
[datetime]::FromFileTime($u."msDS-UserPasswordExpiryTimeComputed")

That printed date must match the Expires column your script produced for that user.

Confirm the task exists and last ran cleanly:

Get-ScheduledTask -TaskName "Notify Expiring AD Passwords"
Get-ScheduledTaskInfo -TaskName "Notify Expiring AD Passwords"   # LastRunTime, LastTaskResult (0 = success)

Confirm mail actually left — check your relay's logs for the send, and look at the expiring-passwords.csv the script writes for the list it processed on each run.

Undo

The script changes nothing in AD, so there is nothing to reverse there. To stop the notifications, disable or remove the scheduled task:

Disable-ScheduledTask -TaskName "Notify Expiring AD Passwords"
# or remove it entirely:
Unregister-ScheduledTask -TaskName "Notify Expiring AD Passwords" -Confirm:$false

Delete the CSV if you don't want the reports lying around. If you sent mail you didn't mean to, there's no recall — which is exactly why the first live run stays scoped to a test OU.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Automate a GPO Backup and Export with PowerShell

This guide sets up a PowerShell script that backs up every Group Policy Object in your domain to a dated folder and, optionally, exports a human-readable HTML report of each one. Backing up GPOs is a read-only operation — Backup-GPO does not change, delete, or unlink anything in Active Directory. It copies the policy settings, security filtering, and WMI filter links into files on disk. The risky part is not the backup; it's the restore , which I cover at the end and which you should treat with real care.

9 min read

Bulk-Update AD User Attributes from a CSV

This guide reads a CSV of users and writes two attributes back to Active Directory for each one: Title and Department . Nothing is created or deleted — existing user objects have those two fields overwritten with the values in your file. That overwrite is a real change: if the CSV has a wrong value in a row, that user's title or department is now wrong until you fix it. There is no built-in "undo," so the safety step below is to export the current values first so you can put them back.

9 min read

Automate User Offboarding in Active Directory with PowerShell

This script offboards one leaving user in a single pass: it disables their AD account, records and removes their group memberships (except the primary group), and moves the account into a disabled-users OU. A separate, clearly marked step sets mail forwarding on their mailbox. The point is a consistent, logged procedure so nothing gets missed and you can reconstruct exactly what changed.

9 min read

Automate New-User Onboarding in Active Directory

This guide builds a PowerShell script that onboards one new employee in four steps: it creates an Active Directory user account , adds them to security groups , provisions an on-premises Exchange mailbox , and creates their home folder on a file server and sets NTFS permissions . The purpose is to replace the error-prone click-through in Active Directory Users and Computers with one repeatable, reviewable run.

10 min read