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
@Edjuha
Copy link

Edjuha commented Nov 30, 2021

Hello!
I would love a little bit help with the script. Whitch variables do i have to change for this script, so the e-mails would be sent exactly at day 3 and day 8?
Thank you!

@Rainer1337-dodo
Copy link

Rainer1337-dodo commented Feb 15, 2022

Greetings!

Just signed-up for this comment since I've stumbled accross this updated script recently.

I'm currently using the "original" script (2.9, August 2018) from Rober Pearman to send out password expiration notifications to our users (hybrid environment; AD users synced to AAD).

However, I'm facing an issue with this script ever since we've moved from pass-through auth to password-hash sync and now the script has some erratic behavior where only sometimes a notification mail is sent out.

As a result, I'd like to try this updated version but it seems I'm missing something here. In the "original", you'd have to import user credentials in order to send out mails (e.g. from a service account called "[email protected]"). We did that using the account's password in clear text, converted to secure string.

Now, with this new script above... I'm unable to send out mails ("could not send email to $adminEmailAddr via smtp.office365.com"). I assume this is because the account that's supposed to send out the notifications needs to be authenticated and some point first? Or is this script not intended to be used with M365 as SMTP?

Thanks for clarifying... I'm not much of a coder/scripter myself, unfortunately.

@cloudcap10
Copy link
Author

Yes required authentication. If you have access to your DNS server you can add your Public IP in SPF record without authentication:

v=spf1 ip4: include:spf.protection.outlook.com ~all

@Rainer1337-dodo
Copy link

Rainer1337-dodo commented Feb 15, 2022

Thanks for the quick reply.

I'd like to stick with providing the credentials within the script. I know it's not ideal since the password is in there in clear text, but it seems like the easiest solution to me that doesn't require a lot of fiddling around.

In the original script, I've used something like this to get the account authenticated that sends out those notifications:

#IMPORT CREDENTIALS "AccountPasswordInClearText" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString | Out-File -FilePath [email protected] New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "[email protected]",(Get-Content -Path [email protected] | ConvertTo-SecureString)

As noted, my understanding of PS scripting is very basic at best... any chance you could provide a simple solution on how to get the account authenticated in this updated version of the script? I guess it wouldn't be enough to simply c&p the code to this version of the script since I somehow need to "tell" the script when to use said credentials.

Best regards!

@cloudcap10
Copy link
Author

cloudcap10 commented Feb 15, 2022

try this:

$SearchBase="DC=EXAMPLE,DC=COM"
$smtpServer="smtp.example.com"
$SMTPPort = "587"
$SMTPUsername = "SMTP Username Here"
$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 = 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 -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 $SMTPCredentials
} 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

Rainer1337-dodo commented Feb 16, 2022

Amazing, thank you very much!

So, I've tried with the updated script you've provided and filled in the configuration requirements:
Script_Config

However, it seems to me that although the credentials are being used as intended, my user account somehow is still unable to send mails via smtp.office365.com (for whatever reason).

Additionally, it seems there's an issue with line #52 in the code as the results suggests:
Script_Results

The log file is created successfully, but it remains empty.

Any ideas? Sorry for being a pain in the ass... I'm not sure what I'm missing here.

@cloudcap10
Copy link
Author

hi Rainer,

You need to run as administrator for your PowerShell.

@Rainer1337-dodo
Copy link

Rainer1337-dodo commented Feb 28, 2022

Hey there,

sorry for my delayed response. I did run PowerShell as administrator from the get-go; unfortunately, still get the exact same error message:

PS C:\Windows\system32> C:\Scripts\PW-Change\PasswordChangeNotificationNEW.ps1
Processing DC=MYDOMAIN,DC=COM for Password-Expiration-Notifications
Testing only; 1 email samples will be sent to [email protected]
Add-Content : A positional parameter cannot be found that accepts argument 'Date,SAMAccountName,DisplayName,Created,PasswordSet,DaystoExpire,ExpiresOn,EmailAddress,Notified'.
At C:\Scripts\PW-Change\PasswordChangeNotificationNEW.ps1:52 char:1

  • Add-Content $logfile ""Date","SAMAccountName","DisplayName","Created" ...
  •   + CategoryInfo          : InvalidArgument: (:) [Add-Content], ParameterBindingException
      + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.AddContentCommand
    
    

Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
Error: Could not send email to [email protected] via smtp.office365.com
149 Users from DC=MYDOMAIN,DC=COM Processed in 0 minutes 4 seconds.
Email trigger range from -1 (past) to 21 (upcoming) days of user's password expiry date.
0 Emails Sent.
0 Emails skipped.
20 Emails failed.
CSV File created at C:\Scripts\PW-Change 2022\LOG_NEW\Log.csv.
Error: Failed to email CSV log to [email protected] via smtp.office365.com

@cloudcap10
Copy link
Author

cloudcap10 commented Feb 28, 2022

my testing is ok. You need make sure these 2 parameters is the same.

$SMTPUsername = "[email protected]" and $from = "Password Expiry Notification<[email protected]>"

created a sample here https://github.com/talzcloning/smtp-powershell/blob/main/Password-Expiration-Notifications-office365.ps1

@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