Skip to content

Instantly share code, notes, and snippets.

@cloudcap10
Forked from njdro/Example.com-Password-Expiration-Notifications.ps1
Last active November 29, 2022 17:00
Show Gist options
  • Select an option

  • Save cloudcap10/9884c37d2361b04a4040129a1a8488a5 to your computer and use it in GitHub Desktop.

Select an option

Save cloudcap10/9884c37d2361b04a4040129a1a8488a5 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:

  • Added Office 365 SMTP - 2-Password-Expiration-Notifications-office365.ps1
  • No SMTP Authentication - 1-Password-Expiration-Notifications.ps1
  • A SearchBase is required.
  • When logging, the CSV will always be overwritten.
  • Accounts with recently-expired passwords can be notified by specifying a "negativedays" value.
  • Email attempts will handle basic errors.
  • 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 expirations.)
  • 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 v20180412
# 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....
$SearchBase="DC=EXAMPLE,DC=COM"
$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 <[email protected]>"
$logging = $true # Set to $false to Disable Logging
$logNonExpiring = $false
$logFile = "c:\PS-pwd-expiry.csv" # ie. c:\mylog.csv
$testing = $true # Set to $false to Email Users
$adminEmailAddr = "[email protected]","[email protected]","[email protected]" #multiple addr allowed but MUST be independent strings separated by comma
$sampleEmails = 1 #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
$starttime=Get-Date #need time also; don't use date from above
Write-Host "Processing `"$SearchBase`" for Password-Expiration-Notifications"
#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`""
}
# Get Users From AD who are Enabled, Passwords Expire
Import-Module ActiveDirectory
$users = get-aduser -SearchBase $SearchBase -Filter {(enabled -eq $true) -and (passwordNeverExpires -eq $false)} -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated
$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
$countprocessed=${users}.Count
$samplesSent=0
$countsent=0
$countnotsent=0
$countfailed=0
# 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>
<p>Thank you,<br>
Example.com Administrator<br>
[email protected]<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."
if ($logging -eq $true) {
#sort the CSV file
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}."
#email the CSV and stats to admin(s)
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
#################################################################################################################
#
# Modified by: talzcloning
#
# 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....
$SearchBase="DC=domain,DC=com"
$smtpServer="smtp.office365.com"
$SMTPPort = "587"
$SMTPUsername = "[email protected]"
$GetPassword = Get-Content "C:\temp\password.txt" #File with password has restricted access
$SMTPPassword = $GetPassword | ConvertTo-SecureString -AsPlainText -Force
$SMTPCredentials = new-object Management.Automation.PSCredential ($SMTPUsername,$SMTPPassword)
$expireindays = 10 #number of days of soon-to-expire paswords. i.e. notify for expiring in X days (and every day until $negativedays)
$negativedays = -1 #negative number of days (days already-expired). i.e. notify for expired X days ago
$from = "Password Expiry <[email protected]>"
$logging = $true # Set to $false to Disable Logging
$logNonExpiring = $false
$logFile = "C:\Logs\PS-pwd-expiry.csv" # ie. c:\mylog.csv
$testing = $true # Set to $false to Email Users
$adminEmailAddr = "[email protected]" #multiple addr allowed but MUST be independent strings separated by comma
$sampleEmails = 1 #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
$starttime=Get-Date #need time also; don't use date from above
Write-Host "Processing `"$SearchBase`" for Password-Expiration-Notifications"
#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`""
}
# Get Users From AD who are Enabled, Passwords Expire
Import-Module ActiveDirectory
$users = get-aduser -SearchBase $SearchBase -Filter {(enabled -eq $true) -and (passwordNeverExpires -eq $false)} -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated
$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
$countprocessed=${users}.Count
$samplesSent=0
$countsent=0
$countnotsent=0
$countfailed=0
# 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="$sName Windows Login 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>
<p>Thank you,<br>
Example.com Administrator<br>
[email protected]<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 -port $SMTPPort -UseSsl -Credential $SMTPCredentials
} 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."
if ($logging -eq $true) {
#sort the CSV file
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}."
#email the CSV and stats to admin(s)
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 -port $SMTPPort -UseSsl -Credential $SMTPCredential
} 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
@Rainer1337-dodo
Copy link

Strange. All parameters seem to be fine; $SMTPUsername == $from

I still think that somehow authentication with the user account seems to fail vs. smtp.office365.com (for whatever reason). I've checked that no MFA is enabled for this specific account as well as no conditional access policy are somehow interfering, but I can't seem to get this script running.

Will try in the upcoming days/weeks when I find the time to further look into this.

Again, thank you very much for helping me out. Much appreciated.

Cheers!

@Rainer1337-dodo
Copy link

hey @talzcloning !

Colleague of mine finally figured it out... seems that you need to force PowerShell to use TLS 1.2:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Put that one at the beginning of the script and it now works like a charm :)

Maybe this helps if someone else faces a similar issue.

Cheers, and once again, thanks for your time & effort!

@DrunkMunki
Copy link

Howdy,
Is there a way to put the contents of the CSV into the email?
I would like to get helpdesk to recieve the emails as a ticket without having to open the CSV

@TKoboldt
Copy link

Excellent script! this is definitely a time saver, very much appreciated.

Hate to be that guy that takes something awesome and asks for something else, but...

Is there a way I can have multiple search bases? I need to have 3 to 5 different OUs to search. I don't want to go all the way to the root, that will get all the service accounts and vendor accounts, etc.

thanks again for the great script.

@cloudcap10
Copy link
Author

For line code 15 change example like these
$SearchBase='OU=IT,OU=Corp Users,DC=talz,DC=cloning','OU=HR,OU=Corp Users,DC=talz,DC=cloning'

For line code 59 change to this
$users = $SearchBase | ForEach-Object { get-aduser -SearchBase $PSItem -Filter {(enabled -eq $true) -and (passwordNeverExpires -eq $false)} -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated }

@spejos
Copy link

spejos commented Aug 31, 2022

Hi,

We would like to use this script as a scheduled task
How can we prevent powershell to prompt for the office 365 credentials in a popup?

Regards

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