Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
  • 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.
  • 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).
#################################################################################################################
#
# Password-Expiration-Notifications v20210428
# Highly Modified fork. https://gist.github.com/meoso/3488ef8e9c77d2beccfd921f991faa64
#
# Originally from v1.4 @ https://gallery.technet.microsoft.com/Password-Expiry-Email-177c3e27
# 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"
$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"
$users = $users | Where-Object {$_.DistinguishedName -notmatch $ExcludeList}
$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
$countprocessed=${users}.Count
$samplesSent=0
$countsent=0
$countnotsent=0
$countfailed=0
#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."
}
$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"
#continue #"continue" would skip user, but bypass any non-expiry logging
}
#Write-Host "$sName DtE: $daystoexpire MPA: $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 -lt $sampleEmails)) {
$recipient = $adminEmailAddr
} else {
$recipient = $emailaddress
}
#if in trigger range, send email
if ( ($daystoexpire -ge $negativedays) -and ($daystoexpire -lt $expireindays) -and ($daystoexpire -ne "NA") ) {
# 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 "$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>
$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
@williamchg

This comment has been minimized.

Copy link

@williamchg williamchg commented Apr 17, 2017

Awesome script. Thank you!

@DelosCorp

This comment has been minimized.

Copy link

@DelosCorp DelosCorp commented Sep 26, 2019

Great script thank you!

@talzcloning

This comment has been minimized.

Copy link

@talzcloning talzcloning commented Nov 29, 2019

Thanks

@C-Zhen

This comment has been minimized.

Copy link

@C-Zhen C-Zhen commented Apr 8, 2020

Thank you!

@Illuzion-glitch

This comment has been minimized.

Copy link

@Illuzion-glitch Illuzion-glitch commented May 5, 2020

Thank you!
If I change $logNonExpiring than I become only log Mails if there are users who has to change the password?
If not how can I change it that not every time I start the script I become a empty log csv?

@Yuzzman

This comment has been minimized.

Copy link

@Yuzzman Yuzzman commented May 25, 2020

Thanks It works great I have just an issue with CSV file which is always empty

@meoso

This comment has been minimized.

Copy link
Owner Author

@meoso meoso commented Jul 16, 2020

  • We have so many users, i never get an empty CSV file.
  • i suspect empty files mean no expiration were found. or possibly access to the file system is prohibited -- check the user-rights or set-executionpolicy rights of the scheduled-task.
  • i tend to schedule with -ExecutionPolicy Bypass -NoProfile -NonInteractive -File "SCRIPT.ps1" parameters
  • An easy way to retain old logs might be: something like $logFile = "c:\PS-pwd-expiry-$(Get-Date -format yyyy-MM-dd).csv"
@giox069

This comment has been minimized.

Copy link

@giox069 giox069 commented Nov 11, 2020

Thank you for this script. Just a minor problem: in line 40 $sampleEmails=$users.Count $users variable is used before it's defined. The "all" option for sample emails is not working.

@jerrigodinho

This comment has been minimized.

Copy link

@jerrigodinho jerrigodinho commented Dec 17, 2020

Hello folks
I use Office365 SMTP, Powershell list the users with password expiration, but didn't receive any email when ran the script with variable $testing = $true
I suspect that needs to implement SMTP authentication on the script. Does anyone help me?

@jerrigodinho

This comment has been minimized.

Copy link

@jerrigodinho jerrigodinho commented Dec 17, 2020

Hello folks
I use Office365 SMTP, Powershell list the users with password expiration, but didn't receive any email when ran the script with variable $testing = $true
I suspect that needs to implement SMTP authentication on the script. Does anyone help me?

Hi guys!
I used this link to solve:
https://vladtalkstech.com/2016/03/send-email-from-powershell-in-office-365.html#:~:text=Luckily%2C%20there%20is%20an%20easy,587%20and%20secured%20by%20SSL.

@meoso

This comment has been minimized.

Copy link
Owner Author

@meoso meoso commented Jan 20, 2021

@giox069, thank you, i'll take a look. i have not used "all" since inception, because i dont want 6000+ sample emails :)
...
i moved the 3 if/then blocks, that should do it, afaik.

@deebabat

This comment has been minimized.

Copy link

@deebabat deebabat commented Jan 29, 2021

Am only trying to email a list of users that I saved in a txt file, please how do I implement that?

@deebabat

This comment has been minimized.

Copy link

@deebabat deebabat commented Feb 1, 2021

Am only trying to email a list of users that I saved in a txt file, please how do I implement that?

Nevermind, I was able to use the list of users stored in the text file.

@luifevm76

This comment has been minimized.

Copy link

@luifevm76 luifevm76 commented Mar 22, 2021

Hello,

I configured the script succesfully and runs well.

I try to run the script on a scheduled task but it hangs "running" and nothing happends.

Im running the script on a DC windows server 2016 std.

Thank you in advance

@luifevm76

This comment has been minimized.

Copy link

@luifevm76 luifevm76 commented Mar 23, 2021

Hello,

I configured the script succesfully and runs well.

I try to run the script on a scheduled task but it hangs "running" and nothing happends.

Im running the script on a DC windows server 2016 std.

Thank you in advance

I found a solution:
https://community.spiceworks.com/how_to/17736-run-powershell-scripts-from-task-scheduler
wachout with spaces on paths

@meoso

This comment has been minimized.

Copy link
Owner Author

@meoso meoso commented Apr 21, 2021

I found a solution:
https://community.spiceworks.com/how_to/17736-run-powershell-scripts-from-task-scheduler
wachout with spaces on paths

Apologies for not checking my email sooner

yes, i have similar notes:

command:
C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe

parameter:
-ExecutionPolicy Bypass -WindowStyle Hidden -File "YOURSCRIPT.ps1"
(or)
-ExecutionPolicy Bypass -NoProfile -NonInteractive -File "YOURSCRIPT.ps1"

@luifevm76

This comment has been minimized.

Copy link

@luifevm76 luifevm76 commented Apr 21, 2021

Thanks

@JohnSenpai523

This comment has been minimized.

Copy link

@JohnSenpai523 JohnSenpai523 commented Apr 28, 2021

Hi All,

Thank you for this script, but I have a problem using it. I configured the variables needed but it seems that it can't detect any users that has a expired or will expiry password. I received the email report and its also empty.

Running it on Windows server 2019 Standard

@meoso

This comment has been minimized.

Copy link
Owner Author

@meoso meoso commented Apr 28, 2021

Hi All,

Thank you for this script, but I have a problem using it. I configured the variables needed but it seems that it can't detect any users that has a expired or will expiry password. I received the email report and its also empty.

Running it on Windows server 2019 Standard

On a powershell commandline, does get-aduser produce a userlist?
When you run the script on commandline, do you get output?
is your searchbase correct? do you have rights to read the searchbase/AD?

i just copy/pasted the above script for testing, set my variables and received proper output for 36000+ users.
i have my $expireindays = 7 & $negativedays = -3 same as the script.
(my email failed, but that is due to running on workstation without email rights, afaik)

@meoso

This comment has been minimized.

Copy link
Owner Author

@meoso meoso commented Apr 28, 2021

All,
minor update to the script: $ExcludeList= and a couple debug Write-Hosts.
I basically copied my production script back into this, and reworked for generic example.

Sorry for any inconvenience if you repeatedly saw notifications. i had to soft reset and force-push to redact private info and mistakes.

@JohnSenpai523

This comment has been minimized.

Copy link

@JohnSenpai523 JohnSenpai523 commented Apr 29, 2021

Hi All,
Thank you for this script, but I have a problem using it. I configured the variables needed but it seems that it can't detect any users that has a expired or will expiry password. I received the email report and its also empty.
Running it on Windows server 2019 Standard

On a powershell commandline, does get-aduser produce a userlist?
When you run the script on commandline, do you get output?
is your searchbase correct? do you have rights to read the searchbase/AD?

i just copy/pasted the above script for testing, set my variables and received proper output for 36000+ users.
i have my $expireindays = 7 & $negativedays = -3 same as the script.
(my email failed, but that is due to running on workstation without email rights, afaik)

Hi

Yes, my search base is correct and I do have access to the AD since I use a admin account. I use I run just this part of the script on PowerShell.

##################################################################################################################
# Please Configure the following variables....
$SearchBase="OU=User HO,DC=Company,DC=com"
$smtpServer="ourSMTPserver-com.mail.protection.outlook.com"
$expireindays = 14 #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 = "Password Notification <donotreply@Company.com>"
$logging = $true # Set to $false to Disable Logging
$logNonExpiring = $false
$logFile = "C:\Users\Admin\Desktop\password expires_log\PS-pwd-expiry-$(Get-Date -format yyyy-MM-dd).csv" # ie. c:\mylog.csv
$testing = $true # Set to $false to Email Users
$adminEmailAddr = "admin@Company.com" #multiple addr allowed but MUST be independent strings separated by comma
$sampleEmails = 20 #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.
#
###################################################################################################################


# 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"
$users = $users | Where-Object {$_.DistinguishedName -notmatch $ExcludeList}
$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge

I just got this from PowerShell.

image

@meoso

This comment has been minimized.

Copy link
Owner Author

@meoso meoso commented Apr 29, 2021

please test this .... remove -and (passwordNeverExpires -eq $false)} ...

$users = get-aduser -SearchBase $SearchBase -Filter {(enabled -eq $true) -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated
$users

$users output should produce some results. maybe passwords do not expire in your environment? if that is the case, they will not normally be selected. this test will include passwordNeverExpires

also make sure you still have empty excludelist variable ($ExcludeList = "") if you dont want to filter any OU's

@JohnSenpai523

This comment has been minimized.

Copy link

@JohnSenpai523 JohnSenpai523 commented Apr 30, 2021

please test this .... remove -and (passwordNeverExpires -eq $false)} ...

$users = get-aduser -SearchBase $SearchBase -Filter {(enabled -eq $true) -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated
$users

$users output should produce some results. maybe passwords do not expire in your environment? if that is the case, they will not normally be selected. this test will include passwordNeverExpires

also make sure you still have empty excludelist variable ($ExcludeList = "") if you dont want to filter any OU's

Hi Thank you for replying. I managed to fix it this really helps a lot. Thank you again!

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