Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save meoso/3488ef8e9c77d2beccfd921f991faa64 to your computer and use it in GitHub Desktop.
Save meoso/3488ef8e9c77d2beccfd921f991faa64 to your computer and use it in GitHub Desktop.
PowerShell Active Directory Password Expiration Email Notification

Password-Expiration-Notifications.ps1 is a powerShell script designed to be run on a schedule to automatically email Active Directory users of soon-to-expire and recently-expired passwords.

This version is a highly modified fork of the original v1.4 by Robert Pearman from https://gallery.technet.microsoft.com/Password-Expiry-Email-177c3e27. Pearman's 2.x version was completely re-written.

New in this version:

  • A SearchBase is required.
  • OU ExcludeList [Commented out in code]
  • When logging, the CSV will always be overwritten. (unless you specify a variable file name such as "c:\PS-pwd-expiry-$(Get-Date -format yyyyMMdd-HHmmss).csv")
  • Accounts with recently-expired passwords can be notified by specifying a "negativedays" value.
  • Email attempts will handle basic errors, but nothing more. This script does not account for SMTP credentials. [Please review July 2022 comments for SMTP auth.]
  • Accounts with MaxPasswordAge 00:00:00 (never) are skipped. (Same as PasswordNeverExpires.)
  • Testing-mode will allow a specified number of sample notifications to be emailed to the Administrator(s). (Rather than defaulting to all users' expiration emails.)
  • Processing information and basic statistics are written to console.
  • When logging, the CSV file and basic statistics will be emailed to the specified Administrator(s).

Self-Help Resources:

Prefer scheduling with a Service account

#################################################################################################################
#
# Password-Expiration-Notifications v20220823
# Highly Modified fork. https://gist.github.com/meoso/3488ef8e9c77d2beccfd921f991faa64
#
# Originally from v1.4 @ https://gallery.technet.microsoft.com/Password-Expiry-Email-177c3e27
# https://windowspoweressentials.com/2017/02/21/powershell-password-reminder-script-updated/
# https://github.com/titlerequired/public
# Robert Pearman (WSSMB MVP)
# TitleRequired.com
# Script to Automated Email Reminders when Users Passwords due to Expire.
#
# Requires: Windows PowerShell Module for Active Directory
#
##################################################################################################################
# Please Configure the following variables....
$testing = $true # Set to $false to Email Users. $true to email samples to administrators only (see $sampleEmails below)
$SearchBase="DC=EXAMPLE,DC=COM"
### PURGING this option; seems to cause issue. # $ExcludeList="'New Employees'|'Separated Employees'" #in the form of "SubOU1|SubOU2|SubOU3" -- possibly needing single quote for OU's with spaces, separate OU's with pipe and double-quote the list.
$smtpServer="smtp.example.com"
$expireindays = 7 #number of days of soon-to-expire paswords. i.e. notify for expiring in X days (and every day until $negativedays)
$negativedays = -3 #negative number of days (days already-expired). i.e. notify for expired X days ago
$from = "Administrator <administrator@example.com>"
$logging = $true # Set to $false to Disable Logging
$logNonExpiring = $false
$logFile = "c:\PS-pwd-expiry.csv" # ie. c:\mylog.csv
$adminEmailAddr = "Admin1@example.com","Admin2@example.com","Admin3@example.com" #multiple addr allowed but MUST be independent strings separated by comma
$sampleEmails = 3 #number of sample email to send to adminEmailAddr when testing ; in the form $sampleEmails="ALL" or $sampleEmails=[0..X] e.g. $sampleEmails=0 or $sampleEmails=3 or $sampleEmails="all" are all valid.
# please edit $body variable within the code
###################################################################################################################
# System Settings
$textEncoding = [System.Text.Encoding]::UTF8
$date = Get-Date -format yyyy-MM-dd #for logfile only
$starttime=Get-Date #need time also; don't use date from above
Write-Host "Processing `"$SearchBase`" for Password-Expiration-Notifications"
Write-Host "Testing Mode: $testing"
# Get Users From AD who are Enabled, Passwords Expire
Import-Module ActiveDirectory
Write-Host "Gathering User List"
$users = get-aduser -SearchBase $SearchBase -Filter {(enabled -eq $true) -and (passwordNeverExpires -eq $false)} -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated
Write-Host "Filtering User List"
### PURGING this option; seems to cause issue. # $users = $users | Where-Object {$_.DistinguishedName -notlike $ExcludeList} ##also try -notmatch, needs heavy testing
$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
$countprocessed=${users}.Count
$samplesSent=0
$countsent=0
$countnotsent=0
$countfailed=0
$nonexpiring=0
Write-Host "${countprocessed} user-accounts selected to iterate."
#set max sampleEmails to send to $adminEmailAddr
if ( $sampleEmails -isNot [int]) {
if ( $sampleEmails.ToLower() -eq "all") {
$sampleEmails=$users.Count
} #else use the value given
}
if (($testing -eq $true) -and ($sampleEmails -ge 0)) {
Write-Host "Testing only; $sampleEmails email samples will be sent to $adminEmailAddr"
} elseif (($testing -eq $true) -and ($sampleEmails -eq 0)) {
Write-Host "Testing only; emails will NOT be sent"
}
# Create CSV Log
if ($logging -eq $true) {
#Always purge old CSV file
Out-File $logfile
Add-Content $logfile "`"Date`",`"SAMAccountName`",`"DisplayName`",`"Created`",`"PasswordSet`",`"DaystoExpire`",`"ExpiresOn`",`"EmailAddress`",`"Notified`""
}
# Process Each User for Password Expiry
foreach ($user in $users) {
$dName = $user.displayName
$sName = $user.sAMAccountName
$emailaddress = $user.emailaddress
$whencreated = $user.whencreated
$passwordSetDate = $user.PasswordLastSet
$sent = "" # Reset Sent Flag
$PasswordPol = (Get-AduserResultantPasswordPolicy $user)
# Check for Fine Grained Password
if (($PasswordPol) -ne $null) {
$maxPasswordAge = ($PasswordPol).MaxPasswordAge
} else {
# No FGPP set to Domain Default
$maxPasswordAge = $DefaultmaxPasswordAge
}
#If maxPasswordAge=0 then same as passwordNeverExpires, but PasswordCannotExpire bit is not set
if ($maxPasswordAge -eq 0) {
Write-Host "$sName : MaxPasswordAge = $maxPasswordAge (i.e. PasswordNeverExpires) but bit not set -- User not selected to receive email."
}
$expiresOn = $passwordsetdate + $maxPasswordAge
$today = (get-date)
if ( ($user.passwordexpired -eq $false) -and ($maxPasswordAge -ne 0) ) { #not Expired and not PasswordNeverExpires
$daystoexpire = (New-TimeSpan -Start $today -End $expiresOn).Days
} elseif ( ($user.passwordexpired -eq $true) -and ($passwordSetDate -ne $null) -and ($maxPasswordAge -ne 0) ) { #if expired and passwordSetDate exists and not PasswordNeverExpires
# i.e. already expired
$daystoexpire = -((New-TimeSpan -Start $expiresOn -End $today).Days)
} else {
# i.e. (passwordSetDate = never) OR (maxPasswordAge = 0)
$daystoexpire="NA"
$nonexpiring += 1
#continue #"continue" would skip user, but bypass any non-expiry logging
}
#Write-Host "$sName : DaysToExpire: $daystoexpire MaxPasswordAge: $maxPasswordAge" #debug
# Set verbiage based on Number of Days to Expiry.
Switch ($daystoexpire) {
{$_ -ge $negativedays -and $_ -le "-1"} {$messageDays = "has expired"}
"0" {$messageDays = "will expire today"}
"1" {$messageDays = "will expire in 1 day"}
default {$messageDays = "will expire in " + "$daystoexpire" + " days"}
}
# Email Subject Set Here
$subject="Your password $messageDays"
# Email Body Set Here, Note You can use HTML, including Images.
$body="
<p>Your Active Directory password for your <b>$sName</b> account $messageDays. After expired, you will not be able to login until your password is changed.</p>
<p>Please visit selfservice.example.com to change your password. Alternatively, on a Windows machine, you may press Ctrl-Alt-Del and select `"Change Password`".</p>
<p>If you do not know your current password, <a href='https://selfservice.example.com/?action=sendtoken'>click here to email a password reset link</a>.</p>
Example.com Administrator<br>
Administrator@example.com<br>
www.example.com/support/<br>
</p>
"
# If testing-enabled and send-samples, then set recipient to adminEmailAddr else user's EmailAddress
if (($testing -eq $true) -and ($samplesSent -le $sampleEmails)) {
$recipient = $adminEmailAddr
} else {
$recipient = $emailaddress
}
#if in trigger range, send email
if ( ($daystoexpire -ge $negativedays) -and ($daystoexpire -le $expireindays) -and ($daystoexpire -ne "NA") ) {
Write-Host "$sName : Selected to receive email: password ${messageDays}"
# Send Email Message
if (($emailaddress) -ne $null) {
if ( ($testing -eq $false) -or (($testing -eq $true) -and ($samplesSent -lt $sampleEmails)) ) {
try {
Send-Mailmessage -smtpServer $smtpServer -from $from -to $recipient -subject $subject -body $body -bodyasHTML -priority High -Encoding $textEncoding -ErrorAction Stop -ErrorVariable err
} catch {
write-host "Error: Could not send email to $recipient via $smtpServer"
$sent = "Send fail"
$countfailed++
} finally {
if ($err.Count -eq 0) {
write-host "Sent email for $sName to $recipient"
$countsent++
if ($testing -eq $true) {
$samplesSent++
$sent = "toAdmin"
} else { $sent = "Yes" }
}
}
} else {
Write-Host "Testing mode: skipping email to $recipient"
$sent = "No"
$countnotsent++
}
} else {
Write-Host "$dName ($sName) has no email address."
$sent = "No addr"
$countnotsent++
}
# If Logging is Enabled Log Details
if ($logging -eq $true) {
Add-Content $logfile "`"$date`",`"$sName`",`"$dName`",`"$whencreated`",`"$passwordSetDate`",`"$daystoExpire`",`"$expireson`",`"$emailaddress`",`"$sent`""
}
} else {
#if ( ($daystoexpire -eq "NA") -and ($maxPasswordAge -eq 0) ) { Write-Host "$sName PasswordNeverExpires" } elseif ($daystoexpire -eq "NA") { Write-Host "$sName PasswordNeverSet" } #debug
# Log Non Expiring Password
if ( ($logging -eq $true) -and ($logNonExpiring -eq $true) ) {
if ($maxPasswordAge -eq 0 ) {
$sent = "NeverExp"
} else {
$sent = "No"
}
Add-Content $logfile "`"$date`",`"$sName`",`"$dName`",`"$whencreated`",`"$passwordSetDate`",`"$daystoExpire`",`"$expireson`",`"$emailaddress`",`"$sent`""
}
}
} # End User Processing
$endtime=Get-Date
$totaltime=($endtime-$starttime).TotalSeconds
$minutes="{0:N0}" -f ($totaltime/60)
$seconds="{0:N0}" -f ($totaltime%60)
Write-Host "$countprocessed Users from `"$SearchBase`" Processed in $minutes minutes $seconds seconds."
Write-Host "Email trigger range from $negativedays (past) to $expireindays (upcoming) days of user's password expiry date."
Write-Host "$nonexpiring Non-Expiring accounts."
Write-Host "$countsent Emails Sent."
Write-Host "$countnotsent Emails skipped."
Write-Host "$countfailed Emails failed."
# sort the CSV file
if ($logging -eq $true) {
Rename-Item $logfile "$logfile.old"
import-csv "$logfile.old" | sort ExpiresOn | export-csv $logfile -NoTypeInformation
Remove-Item "$logFile.old"
Write-Host "CSV File created at ${logfile}."
if ($testing -eq $true) {
$body="<b><i>Testing Mode.</i></b><br>"
} else {
$body=""
}
$body+="
CSV Attached for $date<br>
$countprocessed Users from `"$SearchBase`" Processed in $minutes minutes $seconds seconds.<br>
Email trigger range from $negativedays (past) to $expireindays (upcoming) days of user's password expiry date.<br>
$nonexpiring Non-Expiring accounts.<br>
$countsent Emails Sent.<br>
$countnotsent Emails skipped.<br>
$countfailed Emails failed.
"
try {
Send-Mailmessage -smtpServer $smtpServer -from $from -to $adminEmailAddr -subject "Password Expiry Logs" -body $body -bodyasHTML -Attachments "$logFile" -priority High -Encoding $textEncoding -ErrorAction Stop -ErrorVariable err
} catch {
write-host "Error: Failed to email CSV log to $adminEmailAddr via $smtpServer"
} finally {
if ($err.Count -eq 0) {
write-host "CSV emailed to $adminEmailAddr"
}
}
}
# End
@meoso
Copy link
Author

meoso commented May 20, 2024

How can I let expired password users and administrators receive emails at the same time?

line 146: $recipient = $emailaddress maybe change this to something like $recipient = $emailaddress + ", " + $adminEmailAddr or similar.

@JaironJunior
Copy link

The Script is working almost 100% here.

For some reason, theres some users that instead of notifying their emails, the e-mails are coming to me, telling me their password is about to expire. Any idea how I could fix this? :(

@nerdCopter
Copy link

The Script is working almost 100% here.

For some reason, theres some users that instead of notifying their emails, the e-mails are coming to me, telling me their password is about to expire. Any idea how I could fix this? :(

check the user's email in the user's object. otherwise, you may have $testing = $true (default) which will email the administrator list. set it to $testing = $false around line 17.

@PolluxDK
Copy link

PolluxDK commented Jun 7, 2024

Please help me understand how this script would react to the following:
If a users account password has expired like 30 days ago and the "$negativedays = -3" the email will not get sent since it's more than 3 days since the password expired?
Also, if I change the $negativedays to like 500, will that cause an email to be sent every day until a users password has expired for +500days, then it stops?

@meoso
Copy link
Author

meoso commented Jun 7, 2024

Please help me understand how this script would react to the following: If a users account password has expired like 30 days ago and the "$negativedays = -3" the email will not get sent since it's more than 3 days since the password expired? Also, if I change the $negativedays to like 500, will that cause an email to be sent every day until a users password has expired for +500days, then it stops?

Correct, at some point the script should stop pestering the user. if the script was active approaching and through the expiration, then the user get notifications. 3 days after expiring, the user stops receiving notifications.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment