Powershell to email Windows users when their AD password is soon to expire

15 Jan

As with most companys, the most frequent support questions are to do with users passwords. We have a lot of remote users that don’t actually login to the system and as passwords are set to expire quite often so they quite frequently find that their password has expired because generally the only alerting you get for this is when you attempt to login to a machine on the domain.

The windows alerting isn’t that fantastic either, and is quite often ignored.

This powershell script checks for any users who have password expiring in the next 7 days and sends them an email. The email then contains instructions on how to change their password with a variety of options.

<# 
.SYNOPSIS
		Notifies users that their password is about to expire.

.DESCRIPTION
    Let's users know their password will soon expire. Details the steps needed to change their password, and advises on what the password policy requires. Accounts for both standard Default Domain Policy based password policy and the fine grain password policy available in 2008 domains.

.NOTES
    Version    	      	: v2.7 - See changelog at http://www.ehloworld.com/596
    Wish list						: Set $DaysToWarn automatically based on Default Domain GPO setting
    										: Description for scheduled task
    										: Verify it's running on R2, as apparently only R2 has the AD commands?
    										: Determine password policy settings for FGPP users
    										: better logging
    Rights Required			: local admin on server it's running on
    Sched Task Req'd		: Yes - install mode will automatically create scheduled task
    Lync Version				: N/A
    Exchange Version		: 2007 or later
    Author       				: M. Ali (original AD query), Pat Richard, Exchange MVP
    Email/Blog/Twitter	: pat@innervation.com 	http://www.ehloworld.com @patrichard
    Dedicated Post			: http://www.ehloworld.com/318
    Disclaimer   				: You running this script means you won't blame me if this breaks your stuff.
    Info Stolen from 		: (original) http://blogs.msdn.com/b/adpowershell/archive/2010/02/26/find-out-when-your-password-expires.aspx
    										: (date) http://technet.microsoft.com/en-us/library/ff730960.aspx
												:	(calculating time) http://blogs.msdn.com/b/powershell/archive/2007/02/24/time-till-we-land.aspx
												: http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/23fc5ffb-7cff-4c09-bf3e-2f94e2061f29/
												: http://blogs.msdn.com/b/adpowershell/archive/2010/02/26/find-out-when-your-password-expires.aspx
												: (password decryption) http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/f90bed75-475e-4f5f-94eb-60197efda6c6/
												: (determine per user fine grained password settings) http://technet.microsoft.com/en-us/library/ee617255.aspx

.LINK     
    http://www.ehloworld.com/318

.INPUTS
		None. You cannot pipe objects to this script
		
.PARAMETER Demo
		Runs the script in demo mode. No emails are sent to the user(s), and onscreen output includes those who are expiring soon.

.PARAMETER Preview
		Sends a sample email to the user specified. Usefull for testing how the reminder email looks.
		
.PARAMETER PreviewUser
		User name of user to send the preview email message to.

.PARAMETER Install
		Create the scheduled task to run the script daily. It does NOT create the required Exchange receive connector.

.PARAMETER NoImages
		When set to $true, sends the email with no images, but keeps all other HTML formatting.
		
.EXAMPLE 
		.\New-PasswordReminder.ps1
		
		Description
		-----------
		Searches Active Directory for users who have passwords expiring soon, and emails them a reminder with instructions on how to change their password.

.EXAMPLE 
		.\New-PasswordReminder.ps1 -demo
		
		Description
		-----------
		Searches Active Directory for users who have passwords expiring soon, and lists those users on the screen, along with days till expiration and policy setting

.EXAMPLE 
		.\New-PasswordReminder.ps1 -Preview -PreviewUser [username]
		
		Description
		-----------
		Sends the HTML formatted email of the user specified via -PreviewUser. This is used to see what the HTML email will look like to the users.

.EXAMPLE 
		.\New-PasswordReminder.ps1 -install
		
		Description
		-----------
		Creates the scheduled task for the script to run everyday at 6am. It will prompt for the password for the currently logged on user. It does NOT create the required Exchange receive connector.

#> 
#Requires -Version 2.0 

[cmdletBinding(SupportsShouldProcess = $true)]
param(
	[parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] 
	[switch]$Demo,
	[parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] 
	[switch]$Install,
	[parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] 
	[switch]$Preview,
	[parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] 
	[string]$PreviewUser,
	[parameter(ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] 
	[bool]$NoImages = $false
)
Write-Verbose "Setting variables"
[string]$Company = "Contoso Ltd"
[string]$OwaUrl = "https://mail.contoso.com"
[string]$PSEmailServer = "10.9.0.11"
[string]$EmailFrom = "Help Desk <helpdesk@contoso.com>"
# Set the following to blank to exclude it from the emails
[string]$HelpDeskPhone = "(586) 555-1010"
# Set the following to blank to remove the link from the emails
[string]$HelpDeskURL = "https://intranet.contoso.com/"
[string]$TranscriptFilename = $MyInvocation.MyCommand.Name + " " + $env:ComputerName + " {0:yyyy-MM-dd hh-mmtt}.log" -f (Get-Date)
[int]$global:UsersNotified = 0
[int]$DaysToWarn = 14
# Below path should be accessible by ALL users who may receive emails. This includes external/mobile users
[string]$ImagePath = "http://www.contoso.com/images/new-passwordreminder.ps1"
[string]$ScriptName = $MyInvocation.MyCommand.Name
[string]$ScriptPathAndName = $MyInvocation.MyCommand.Definition
[string]$ou
# Change the following to alter the format of the date in the emails sent
# See http://technet.microsoft.com/en-us/library/ee692801.aspx for more info
[string]$DateFormat = "d"

if ($PreviewUser){
	$Preview = $true
}

Write-Verbose "Defining functions"
function Set-ModuleStatus { 
	[cmdletBinding(SupportsShouldProcess = $true)]
	param	(
		[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true, HelpMessage = "No module name specified!")] 
		[string]$name
	)
	if(!(Get-Module -name "$name")) { 
		if(Get-Module -ListAvailable | ? {$_.name -eq "$name"}) { 
			Import-Module -Name "$name" 
			# module was imported
			return $true
		} else {
			# module was not available (Windows feature isn't installed)
			return $false
		}
	}else {
		# module was already imported
		return $true
	}
} # end function Set-ModuleStatus

function Remove-ScriptVariables {  
	[cmdletBinding(SupportsShouldProcess = $true)]
	param(
		[string]$path
	)
	$result = Get-Content $path |  
	ForEach { 
		if ( $_ -match '(\$.*?)\s*=') {      
			$matches[1]  | ? { $_ -notlike '*.*' -and $_ -notmatch 'result' -and $_ -notmatch 'env:'}  
		}  
	}  
	ForEach ($v in ($result | Sort-Object | Get-Unique)){		
		Remove-Variable ($v.replace("$","")) -EA 0
	}
} # end function Remove-ScriptVariables

function Install	{
	[cmdletBinding(SupportsShouldProcess = $true)]
	param()
	# http://technet.microsoft.com/en-us/library/cc725744(WS.10).aspx
	$error.clear()
	Write-Host "Creating scheduled task `"$ScriptName`"..."
	$TaskCreds = Get-Credential("$env:userdnsdomain\$env:username")
	$TaskPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($TaskCreds.Password))
	schtasks /create /tn $ScriptName /tr "$env:windir\system32\windowspowershell\v1.0\powershell.exe -command $ScriptPathAndName" /sc Daily /st 06:00 /ru $TaskCreds.UserName /rp $TaskPassword | Out-Null
	if (! $error){
		Write-Host "Installation complete!" -ForegroundColor green
	}else{
		Write-Host "Installation failed!" -ForegroundColor red
	}
	remove-variable taskpassword
	exit
} # end function Install

function Get-ADUserPasswordExpirationDate {
	[cmdletBinding(SupportsShouldProcess = $true)]
	Param (
		[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, HelpMessage = "Identity of the Account")]
		[Object]$accountIdentity
	)
	PROCESS {
		Write-Verbose "Getting the user info for $accountIdentity"
		$accountObj = Get-ADUser $accountIdentity -properties PasswordExpired, PasswordNeverExpires, PasswordLastSet, name, mail
		# Make sure the password is not expired, and the account is not set to never expire
    Write-Verbose "verifying that the password is not expired, and the user is not set to PasswordNeverExpires"
    if (((!($accountObj.PasswordExpired)) -and (!($accountObj.PasswordNeverExpires))) -or ($PreviewUser)) {
    	Write-Verbose "Verifying if the date the password was last set is available"
    	$passwordSetDate = $accountObj.PasswordLastSet     	
      if ($passwordSetDate -ne $null) {
      	$maxPasswordAgeTimeSpan = $null
        # see if we're at Windows2008 domain functional level, which supports granular password policies
        Write-Verbose "Determining domain functional level"
        if ($global:dfl -ge 4) { # 2008 Domain functional level
          $accountFGPP = Get-ADUserResultantPasswordPolicy $accountObj
          if ($accountFGPP -ne $null) {
          	$maxPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge
					} else {
						$maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
					}
				} else { # 2003 or ealier Domain Functional Level
					$maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
				}				
				if ($maxPasswordAgeTimeSpan -eq $null -or $maxPasswordAgeTimeSpan.TotalMilliseconds -ne 0) {
					$DaysTillExpire = [math]::round(((New-TimeSpan -Start (Get-Date) -End ($passwordSetDate + $maxPasswordAgeTimeSpan)).TotalDays),0)
					if ($preview){$DaysTillExpire = 1}
					if ($DaysTillExpire -le $DaysToWarn){
						Write-Verbose "User should receive email"
						$PolicyDays = [math]::round((($maxPasswordAgeTimeSpan).TotalDays),0)
						if ($demo)	{Write-Host ("{0,-25}{1,-8}{2,-12}" -f $accountObj.Name, $DaysTillExpire, $PolicyDays)}
            # start assembling email to user here
						$EmailName = $accountObj.Name						
						$DateofExpiration = (Get-Date).AddDays($DaysTillExpire)
						$DateofExpiration = (Get-Date($DateofExpiration) -f $DateFormat)						

Write-Verbose "Assembling email message"						
[string]$emailbody = @"
		
"@

if (!($NoImages)){
$emailbody += @"
"@						

if ($HelpDeskURL){		
$emailbody += @"
"@
}else{
$emailbody += @"
"@
}

$emailbody += @"
Description: $ImagePath/spacer.gif
Description: $ImagePath/header.gif
Description: $ImagePath/header.gif
Description: $ImagePath/spacer50.gif Description: $ImagePath/spacer.gif “@ } if ($DaysTillExpire -le 1){ $emailbody += @”

“@ if (!($NoImages)){ $emailbody += @” “@ } $emailbody += @” “@ if (!($NoImages)){ $emailbody += @” “@ } $emailbody += @”

Description: $ImagePath/image001b.gif ALERT: You must change your password today or you will be locked out! Description: $ImagePath/image005b.gif

“@ } $emailbody += @”

Hello, $EmailName,

It’s change time again! Your $company password expires in  $DaysTillExpire  day(s), on $DateofExpiration.

Please use one of the methods below to update your password:

  1. $company office computers and Terminal Server users: You may update your password on your computer by pressing Ctrl-Alt-Delete and selecting ‘Change Password’ from the available options. If you use a $company laptop in addition to a desktop PC, be sure and read #3 below.
  2. Remote Outlook Client, Mac, and/or Outlook Web App users: If you only access our email system, please use the following method to easily change your password:
    • Log into Outlook Web App using Internet Explorer (PC) or Safari or Firefox (Mac).
    • Click on the Options button in the upper right corner of the page.
    • Select the “Change Password” link to change your password.
    • Enter your current password, then your new password twice, and click Save
    • NOTE: You will now need to use your new password when logging into Outlook Web App, Outlook 2010, SharePoint, Windows Mobile (ActiveSync) devices, etc. Blackberry Enterprise Users (BES) will not need to update their password. Blackberry Internet Service (BIS) users will be required to use their new password on their device.
  3. $company issued laptops: If you have been issued a $company laptop, you must be in a corporate office and directly connected to the company network to change your password. If you also use a desktop PC in the office, you must remember to always update your domain password on the laptop first. Your desktop will automatically use the new password.
    • Log in on laptop
    • Press Ctrl-Alt-Delete and select ‘Change Password’ from the available options.
    • Make sure your workstation (if you have one) has been logged off any previous sessions so as to not cause conflict with your new password.

Think you’ve got a complex password? Run it through the The Password Meter

Think your password couldn’t easily be hacked? See how long it would take: How Secure Is My Password

Remember, if you do not change your password before it expires on $DateofExpiration, you will be locked out of all $company Computer Systems until an Administrator unlocks your account.

If you are traveling or will not be able to bring your laptop into the office before your password expires, please call the number below for additional instructions.

You will continue to receive these emails daily until the password is changed or expires.

Thank you,
The $company Help Desk
$HelpDeskPhone

“@ if ($accountFGPP -eq $null){ $emailbody += @”

$company Password Policy

  • Your password must have a minimum of a $MinPasswordLength characters.
  • You may not use a previous password.
  • Your password must not contain parts of your first, last, or logon name.
  • Your password must be changed every $PolicyDays days.
  • “@ if ($PasswordComplexity){ Write-Verbose “Password complexity” $emailbody += @”

  • Your password requires a minimum of two of the following three categories:
    • 1 upper case character (A-Z)
    • 1 lower case character (a-z)
    • 1 numeric character (0-9)
  • “@ } $emailbody += @”

  • You may not reuse any of your last $PasswordHistory passwords

“@ } if (!($NoImages)){ $emailbody += @”

Description: $ImagePath/spacer50.gif
Description: $ImagePath/spacer.gif This email was sent by an automated process. “@ } if ($HelpDeskURL){ $emailbody += @” If you would like to comment on it, please visit click here “@ } if (!($NoImages)){ $emailbody += @” Description: $ImagePath/spacer.gif
"@
}
$emailbody += @"
	

"@
						if (!($demo)){
							$emailto = $accountObj.mail
							if ($emailto){
								Write-Verbose "Sending demo message to $emailto"
								Send-MailMessage -To $emailto -Subject "Your password expires in $DaysTillExpire day(s)" -Body $emailbody -From $EmailFrom -Priority High -BodyAsHtml
								$global:UsersNotified++
							}else{
								Write-Verbose "Can not email this user. Email address is blank"
							}
						}
					}
				}
			}
		}
	}
} # end function Get-ADUserPasswordExpirationDate

if ($install){
	Write-Verbose "Install mode"
	Install
	Exit
}

Write-Verbose "Checking for ActiveDirectory module"
if ((Set-ModuleStatus ActiveDirectory) -eq $false){
	$error.clear()
	Write-Host "Installing the Active Directory module..." -ForegroundColor yellow
	Set-ModuleStatus ServerManager
	Add-WindowsFeature RSAT-AD-PowerShell
	if ($error){
		Write-Host "Active Directory module could not be installed. Exiting..." -ForegroundColor red; 
		if ($transcript){Stop-Transcript}
		exit
	}
}
Write-Verbose "Getting Domain functional level"
$global:dfl = (Get-AdDomain).DomainMode
# Get-ADUser -filter * -properties PasswordLastSet,EmailAddress,GivenName -SearchBase "OU=Users,DC=domain,DC=test" |foreach {
if (!($PreviewUser)){
	if ($ou){
		Write-Verbose "Filtering users to $ou"
		$users = Get-AdUser -filter * -SearchScope subtree -SearchBase $ou -ResultSetSize $null
	}else{
		$users = Get-AdUser -filter * -ResultSetSize $null
	}
}else{
	Write-Verbose "Preview mode"
	$users = Get-AdUser $PreviewUser
}
if ($demo){
	Write-Verbose "Demo mode"
	# $WhatIfPreference = $true
	Write-Host "`n"
	Write-Host ("{0,-25}{1,-8}{2,-12}" -f "User", "Expires", "Policy") -ForegroundColor cyan
	Write-Host ("{0,-25}{1,-8}{2,-12}" -f "========================", "=======", "===========") -ForegroundColor cyan
}

Write-Verbose "Setting event log configuration"
[object]$evt = new-object System.Diagnostics.EventLog("Application")
[string]$evt.Source = $ScriptName
$infoevent = [System.Diagnostics.EventLogEntryType]::Information
[string]$EventLogText = "Beginning processing"
$evt.WriteEntry($EventLogText,$infoevent,70)

Write-Verbose "Getting password policy configuration"
$DefaultDomainPasswordPolicy = Get-ADDefaultDomainPasswordPolicy
[int]$MinPasswordLength = $DefaultDomainPasswordPolicy.MinPasswordLength
# this needs to look for FGPP, and then default to this if it doesn't exist
[bool]$PasswordComplexity = $DefaultDomainPasswordPolicy.ComplexityEnabled
[int]$PasswordHistory = $DefaultDomainPasswordPolicy.PasswordHistoryCount

ForEach ($user in $users){
	Get-ADUserPasswordExpirationDate $user.samaccountname
}

Write-Verbose "Writing summary event log entry"
$EventLogText = "Finished processing $global:UsersNotified account(s). `n`nFor more information about this script, run Get-Help .\$ScriptName. See the blog post at http://www.ehloworld.com/318."
$evt.WriteEntry($EventLogText,$infoevent,70)

# $WhatIfPreference = $false

Remove-ScriptVariables -path $ScriptPathAndName

The source files can be found here.