- Export and import to and from Excel with the PowerShell module ImportExcel - Thu, Apr 21 2022
- Getting started with the PSReadLine module for PowerShell - Thu, Feb 24 2022
- SecretsManagement module for PowerShell: Save passwords in PowerShell - Tue, Dec 22 2020
You know the thought process: regular password expiration is supposed to make organizations safer. However, password expiration also generates calls to the helpdesk when users forget to change passwords before the expiration occurs. One easy way to deal with users forgetting to change their passwords is notify them shortly before expiration. Today, I will show you one example of doing this via an automated email reminder that a nightly PowerShell script generates via a scheduled task.
The reminder emails are straightforward to generate once we figure out some parameters to use for finding passwords about to expire. In my example, I will check for the password expiration date of all Active Directory accounts but skip checking any accounts that have non-expiring passwords, null passwords, or disabled ones. The script will then send emails to the users seven days prior to password expiration, followed by three days prior and then finally one day prior to password expiration.
My script consists of four major parts:
- Getting the password expiration date for each user,
- Calculating the days remaining until password expiration,
- Configuring the mail message to send, and
- Sending the email message.
The last piece is to set up the script to run regularly. We can do this by setting up a scheduled task to run the script. But I will not be covering the configuration of the scheduled task because it is easy to do and there are a ton of references to follow.
We're going to walk through each part of the script in detail, and then we'll put it all together at the end.
Getting the password expiration date for each user
The first step in creating the script is to query all user accounts and expose their password expiration dates:
$users = Get-ADUser -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False -and PasswordLastSet -gt 0} ` -Properties "Name", "EmailAddress", "msDS-UserPasswordExpiryTimeComputed" | ` Select-Object -Property "Name", "EmailAddress", ` @{Name = "PasswordExpiry"; Expression = {[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed").tolongdatestring() }}
Let's break this search down into smaller chunks that make it easier to understand:
Get-ADUser -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False ‑and PasswordLastSet -gt 0}
Here we are searching for the following:
- Enabled users
- Users who do not have a password that never expires
- Users who have a password set (PasswordLastSet -gt 0) because we want to skip users with null passwords
TIP: You could customize this search many ways. Two examples would be to target a specific organizational unit (OU) or maybe a set of accounts that match a name (such as admin accounts).
Then we ask for specific properties to return; we need the EmailAddress for later on. The value msDS-UserPasswordExpiryTimeComputed contains the user's password expiration date. However, the password expiry time is not in a human-readable form, so we have to do a conversion:
@{Name = "PasswordExpiry"; Expression = {[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed").tolongdatestring() }}
This creates a hash table and converts the time to a human-readable format. It saves the value into the variable named PasswordExpiry. Notice at the end I use tolongdatestring. This returns the long format of the date (Sunday, March 25, 2018). I chose this because I thought it would be the most useful to display the day and date for my end users. Also, be aware that I deliberately did not display the actual time of the password expiration, only the day. Dropping the actual time makes it easier to do the comparison for password expiration.
Calculating days remaining until password expiration
The date calculation comes from a simple date match. I could have done subtraction of today's date versus some future dates, but I thought a date match would be simpler. To perform the date match we need to calculate what the dates are one, three, and seven days from today. I calculated the dates by adding the number of days (1, 3, 7) to today's date and saved them to three separate variables for later use in date comparisons.
$SevenDayWarnDate = (get-date).adddays(7).ToLongDateString() $ThreeDayWarnDate = (get-date).adddays(3).ToLongDateString() $OneDayWarnDate = (get-date).adddays(1).ToLongDateString()
Now that we have the dates, we can compare the password expiry date to the dates in the three variables.
if ($user.PasswordExpiry -eq $SevenDayWarnDate)
You can see above that I started an IF statement. For this script, I use an IF/ELSE statement because the password expiration date can only match one day at a time. In other words, a password that expires in three days can only match the three-day warning date. Because of this, an IF/ELSE statement makes it easy to compare the dates and keeping moving until it finds a match.
The descriptive way to read the match statement is like this:
- Check to see if the expiry date is same is the same as the seven-day warn date; if it is then run a command, otherwise…
- Check whether the password expiry date matches the three-day warn date, then run a command, otherwise…
- Check whether the password expiry date matches the one-day warn date, then run a command, otherwise…
- Skip this user and move on to the next one.
Here is the IF/ELSE statement simplified:
foreach ($user in $users) { if ($user.PasswordExpiry -eq $SevenDayWarnDate) { RUN A COMMAND } elseif ($user.PasswordExpiry -eq $ThreeDayWarnDate) { RUN A COMMAND } elseif ($user.PasswordExpiry -eq $oneDayWarnDate) { RUN A COMMAND } else {}
Configuring the mail message sent
The cmdlet to send mail via PowerShell is aptly named Send-MailMessage, and the syntax is easy to understand.
Look at the cmdlet syntax below, and notice the cmdlet is looking for string values.
SYNTAX Send-MailMessage [-To] <String[]> [-Subject] <String> [[-Body] <String>] [[‑SmtpServer] <String>] [-Attachments <String[]>] [-Bcc <String[]>] [-BodyAsHtml] [-Cc <String[]>] [-Credential <PSCredential>] [-DeliveryNotificationOption {None | OnSuccess | OnFailure | Delay | Never}] [‑Encoding <Encoding>] -From <String> [-Port <Int32>] [-Priority {Normal | Low | High}] [-UseSsl] [<CommonParameters>]
The email message I would like to send to my end users is:
I am a bot and performed this action automatically. I am here to inform you that the password for USERNAME will expire in X days on Long Date. Please contact the helpdesk if you need assistance changing your password. DO NOT REPLY TO THIS EMAIL.
The bold text represents variables. We must do some sting manipulation when trying to put variables in the middle of strings; I chose to use joins. I created some email variables to hold the string text for the email. Each string contains text up to the point where we will place a variable.
$EmailStub1 = 'I am a bot and performed this action automatically. I am here to inform you that the password for' $EmailStub2 = 'will expire in' $EmailStub3 = 'days on' $EmailStub4 = '. Please contact the helpdesk if you need assistance changing your password. DO NOT REPLY TO THIS EMAIL.'$days = 3 $EmailBody = $EmailStub1, $user.name, $EmailStub2, $days, $EmailStub3, $SevenDayWarnDate, $EmailStub4 -join ' '
The join at the end joins the strings and variables together and separates them each with a single space. Pay close attention to the $days variable. It makes the email message customized for each date match (1, 3, and 7 days).
Sending the message
Here you can see my mail send syntax; it's nothing complicated. Even though it's a generic mail send message, the join statement earlier customizes the email body with the right number of days until expiration. Again, I created some variables to hold the important mail server information.
$MailSender = " Password AutoBot <EMAILADDRESS@SOMECOMPANY.com>" $Subject = 'FYI - Your account password will expire soon' $SMTPServer = 'smtp.somecompany.com'
Send-MailMessage -To $user.EmailAddress -From $MailSender -SmtpServer $SMTPServer -Subject $Subject -Body $EmailBody
Now, let's put it all together into a complete script:
## Send-PasswordExpiryEmails.ps1 ## Author: Mike Kanakos - www.networkadm.in ## Created: 2018-03-23 ## ----------------------------- #Import AD Module Import-Module ActiveDirectory #Create warning dates for future password expiration $SevenDayWarnDate = (get-date).adddays(7).ToLongDateString() $ThreeDayWarnDate = (get-date).adddays(3).ToLongDateString() $OneDayWarnDate = (get-date).adddays(1).ToLongDateString() #Email Variables $MailSender = " Password AutoBot <emailaddress@somecompany.com>" $Subject = 'FYI - Your account password will expire soon' $EmailStub1 = 'I am a bot and performed this action automatically. I am here to inform you that the password for' $EmailStub2 = 'will expire in' $EmailStub3 = 'days on' $EmailStub4 = '. Please contact the helpdesk if you need assistance changing your password. DO NOT REPLY TO THIS EMAIL.' $SMTPServer = 'smtp.somecompany.com' #Find accounts that are enabled and have expiring passwords $users = Get-ADUser -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False -and PasswordLastSet -gt 0 } ` -Properties "Name", "EmailAddress", "msDS-UserPasswordExpiryTimeComputed" | Select-Object -Property "Name", "EmailAddress", ` @{Name = "PasswordExpiry"; Expression = {[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed").tolongdatestring() }} #check password expiration date and send email on match foreach ($user in $users) { if ($user.PasswordExpiry -eq $SevenDayWarnDate) { $days = 7 $EmailBody = $EmailStub1, $user.name, $EmailStub2, $days, $EmailStub3, $SevenDayWarnDate, $EmailStub4 -join ' ' Send-MailMessage -To $user.EmailAddress -From $MailSender -SmtpServer $SMTPServer -Subject $Subject -Body $EmailBody } elseif ($user.PasswordExpiry -eq $ThreeDayWarnDate) { $days = 3 $EmailBody = $EmailStub1, $user.name, $EmailStub2, $days, $EmailStub3, $ThreeDayWarnDate, $EmailStub4 -join ' ' Send-MailMessage -To $user.EmailAddress -From $MailSender -SmtpServer $SMTPServer -Subject $Subject ` -Body $EmailBody } elseif ($user.PasswordExpiry -eq $oneDayWarnDate) { $days = 1 $EmailBody = $EmailStub1, $user.name, $EmailStub2, $days, $EmailStub3, $OneDayWarnDate, $EmailStub4 -join ' ' Send-MailMessage -To $user.EmailAddress -From $MailSender -SmtpServer $SMTPServer -Subject $Subject -Body $EmailBody } else {} }
Here's what an actual email from the script looks like after processing:
The next step would be to set up a scheduled task and run the script daily at certain time. You can copy this script and use it as is, or you can use it as a starting block and customize it to your needs. Once configured, it should greatly reduce the number of calls to the helpdesk for assistance with expired passwords.
Following the chunks of code is not fun. If we could download the entire script we could better see the flow.
Never mind, found it.
Ey where did you find it?
I also would love to know because the script we have is very robust and provides a lot more than what we need and we are looking to trim it down to something like this.
LJ,
The script is embedded in the article. Look for the line that says “Click to Expand code”
I have an updated version of this script completed. I can post my github repo later this week and then post the link here as well.
Don,
Thanks for the feedback.
My intention greater than just sharing a script. Instead, I was hoping to show people how to build up a few simple ideas into something a little more complex. Maybe next time I will call out early on that the complete script is at the bottom of the article.
SIDE NOTE: Some of the article text got copied into the final script block. I have since corrected the error. Previously, the first 8 lines were actual text from the article and were probably confusing.
Sorry if that caused confusion for any readers.
Nice article Don! Well done!
I’m not confused by your article by the way! 🙂
Can I suggest few way of improving your script?
I’ve created similar scripts in almost a compact script format (almost a one-liner) using JSON file for storing my variables, so the script can be signed once after being tested (with pester and simple unit-tests) an can be safely executed knowing that the code wasn’t altered after being deployed, meeting the needs of more restricting execution policies.
So my first suggestion is decouple where possible settings from the script itself, so is easier to re-use in different environments.
Second is using require statements, if you’re script depends from a module or running as an administrator will it make easier to read and debug.
#Requires –Modules ActiveDirectory
Third is stick with the DRY approach (Don’t Repeat Yourself). You’re design implicitly has some different warning dates (1,3,7), but they are Hard-Coded, if you wan’t to remove or add dates you need to alter many lines of code. That is key on preserving your code for a long time without any need to change. Focus some effort on refactoring.
$WarningDates = 1,3,7
Use a foreach and a define a function send warning and look how more readable your code will be:
#check password expiration date and send email on match
$WarningDays = 1,3,7
foreach ($user in $users) {
foreach($WarnDay in $WarningDays){
if($user.PasswordExpiry -eq (get-date).adddays($WarnDay).ToLongDateString()) {
$EmailBody=$EmailStub1,$user.name,$EmailStub2,$WarnDay,$EmailStub3,$SevenDayWarnDate,$EmailStub4-join’ ‘
Send-MailMessage-To $user.EmailAddress-From$MailSender-SmtpServer $SMTPServer-Subject $Subject`
-Body $EmailBody
}
}
}
Forth… joinin strings is a good strategy, but not the best in this case because the content of the email is not easy to read. Why don’t you create a simple $EmailBody variable?
$email_body = “Hi $($user.name), … some text”
Or my favourite is create different body templates (so I can eventually choose it according to the user or day) and replacing a string “-USERNAME-“. Similar to this
$email_body = “Hi -USERNAME-, … some text”.Replace(“-USERNAME-“, $user.name))
And you can also concatenate replace methods if needed.
Fifth if you’re going to schedule this task you need to provide exit values for knowing from task scheduler when script is executed correctly.
Sixth, try to avoid comments. Yes, I’ve said it… Comments are less useful than you think, personally I try to write code is easy to read for everybody.. even a novice of programming.
Seven, review your code and try to make it shorter and simpler. Refactoring your own code will make you a better developer.
Sorry if I wrote too much, I really think that your article is good, I thought that with some small steps can be even simpler and efficient and make it great.
Hi Paolo!
Thanks for taking the time to give me feedback! I love your ideas.
I know that I still can improve my scripts. My biggest challenge is not thinking about parameterizing my scripts enough.
I had wanted to do something with the email body like you outlined but somehow I found myself using joins. I guess I got away from my original idea as I got lost in my code. Writing these articles pushes me and I like that, but I fully expect someone (like yourself) to post a comment for every article that says something like, “Hey, here’s what you could have done better”.
Again, thanks for the feedback. I am very happy to hear that the article was easy to follow.
Thanks Mike, Paolo! Very nice ideas. I am going to play with it. It is so cool. I think I can use the same concept to send an email to an admin when a user account is locked, or some network/server issues.
I agree with Paolo about using arrays and variables (email). I would actually create a function for creating the email out of strings or events. It can be reused elsewhere.
One thing that I recently learnt from somewhere is to add some some common functions to a power shell Module and load it to your environment (e.g., ISE). I am going to add an email function that I can use in any of my scripts. Thanks for the tip!
I really doubt if there is ever a “the script”! Scripts evolve – get refined and diversified as we learn, explore possibilities and face challenges. But it is the idea that rules over and makes you explore.
Paolo do you have an article on JSON in PowerShell? I’d love to see.
Use a here-string to build the email message body. MUCH cleaner. Also, you can do some extra work and find the requirements for password complexity to include that info in the email message (“Your password must be at least 8 characters….”).
Pat,
Thanks for the feedback and I agree I could definitely improve on the script. Probably release a v2 of the script n the near future.
If you need some code examples, see my version on Github: https://github.com/patrichard/New-AdPasswordReminder
Great script just what I was looking for, I followed your suggestion and targeted a specific OU like so,
Get-ADUser -SearchBase OU=IT,OU=Root,OU,DC=ChildDomain,DC=RootDomain,DC=com” -Filter *
thanks for taking the time to write the article.
Great I was able to user this to create my own script for specific OUs.
Question
Is there a way to output the data that the script creates to a file so you can see data and who the emails were sent to?
I think time span could have been used with greater effect rather comparing on long date strings
Since it would allow you to compare based on days and you could also uses the days number when sending the email
Plus your script is very verbose, consider using a function to generalise sending the email.
You can also use multiline string to cleanup the email body
Example of sending email
$smtp = "smtpserver.lab.local" $to = "$($user.DisplayName) <$($user.UserPrincipalName)>" $from = "passwordReminder@lab.local" $subject = "Password reminder" $body = @" <p>Hi $($user.DisplayName)</p> <p>Your password will expire in <strong>$($user.PasswordExpirationTimeSpan.Days) days</strong></p> <p>Please contact the helpdesk if you need assistance changing your password.</p> "@ Send-MailMessage -SmtpServer $smtp -Bcc $from -To $to -From $from -Subject $subject -Body $body -BodyAsHtml -Priority High -Encoding ([System.Text.Encoding]::UTF8)
Hi Joseph,Thanks for the feedback. You’re right, I could have done some things better for sure. This script was completely re-written by me late in 2018. I need to post a link to the updated script. However, I do appreciate the code you wrote in your reply.
One thing the script also missed was when someone password was already expired 😅
Would mind cleaning up my code snippet? 😅
it was meant to say the following in the foreach loop in the first snippet
As for second snippet
Your welcome to delete this reply 🙂
Joseph,
I’ll clean it up sometime tomorrow.
Amazing script and thank you for taking the time to break it down for me as well.
As a PowerShell newbie, I thank you dearly
Greetings,
Sorry to revive an old conversation, I've been using this script for a number of years with great success, however i've seen from Microsoft's documentation that the send-mailmessage is obsolete now, and doesn't promise a secure connection to SMTP servers (https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/send-mailmessage?view=powershell-7.1).
Do you have any alternative suggestions for the same functionality?
Hi Kevin,
I too still use Send-mailmessage. There is a .net alternative but i need to do some research into that before i can share how to use that code.
Dr. Tobias Weltner put up an impassioned argument to keep Send-MailMessage on the PS GitHub. I can't think of a single reason why it would be deprecated. I've seen it used all over the place – it's virtually always used to send notifications from scripts to admins. I don't think anyone cares if it's send insecurely by SMTP across the local intranet for a notification. If they really do deprecate it, then I think we'll see a huge uptick in the use of things like blat – we're scripting admins, we just do not care if our notification is a secured email.
David F.
My question is more how to go about testing the use of the script effectively in an Active Directory Domain. I have created an OU called (COMP) Test Password Expiration policy and added my computer to this OU. This OU has a GPO with Maximum password age set to 8 days. I have blocked inheritance on this OU from the Default Domain Policy so as not to have conflicting password policies. I verified that my computer is picking up the policy.
I set my PasswordLastSet day on my Active Directory account to 5 days ago. When I run the following part of the code:
Get-ADUser -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False -and PasswordLastSet -gt 0 } `
-Properties "Name", "EmailAddress", "msDS-UserPasswordExpiryTimeComputed" | Select-Object -Property "Name", "EmailAddress", `
@{Name = "PasswordExpiry"; Expression = {[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed").tolongdatestring() }}
I show a PasswordExpiry of Monday, February 28, 2022. I'm probably missing something simpler, or if there is a better way to test please share. Thanks in advance!!
Hi, thank you for the script, I am about to use it for a client of mine however if you have an updated version can you post the sample please? I will be targeting a specific OU for testing purposes thereafter the whole of AD.
Hoping someone is still monitoring this thread. Is there a way to embed our logo in the email at the bottom ?
Hi CB
You can add HTML to the body of the email and embed a logo. I’ll see if I can find a working example for you.
Thanks Mike I found I can add a logo but not with the body part of code.
Currently receiving an error while testing. Send-MailMessage -To emailaddress@email.com -From $MailSender -SmtpServer $SMTPServer -Subject $Subject -Body $EmailBody
Send-MailMessage : Cannot convert ‘System.Object[]’ to the type ‘System.String’ required by parameter ‘Body’. Specified method is not supported.
At line:1 char:120
+ … MailSender -SmtpServer $SMTPServer -Subject $Subject -Body $EmailBody
+ ~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Send-MailMessage], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.SendMailMessage
I can change -body to -BodyAsHtml “” -Attachments “C:\logo.jpg”
and it sends an inline logo and I tried -Body $EmailBody -BodyAsHtml “” -Attachments “C:\logo.jpg” and it sends but that errors on the -body $EmailBody still??
Thanks Mike error resolved you can delete the above comment. Had typo from messing with the script.
This may be a bit off scope but I was wondering if scripts like these could be make to use in Windows Task Scheduler? I ask because I have a password expiration reminder PS Script that I am trying to get to work in Task Scheduler but something tells me I might not be able to do it with a Gmail account that is set up for 2-factor authentication. I looked into the app password setting but I find that a bit confusing.
Any suggestions would be much appreciated.