NB! Old article warning! This is 85 % obsolete as of 2021-10-13 when this article is being recreated on a new web server/site.
The difference between the first and last warning is that there's an exclamation point after the subject of the last warning mail, for the sake of emphasis. Also, it will be logged as either "first warning mail sent" or "last warning mail sent".
You can write a custom message for the first and the last warning, and they can be different (or the same). Plain text files are used. Use the configuration file to configure which values the script uses for SMTP server, port, how many days before the passwords expire to warn, the "from" e-mail address, whether it should be run with authentication or not - and if SSL should be used. I've only added the option of using the logged on user's credentials or sending unauthenticated. I figure this will normally be run as a scheduled task on some server, not interactively. Use runas.exe or something similar if need be.I recommend setting it to run hidden and of course "whether the user is logged on or not". Depending on user object permissions in Active Directory, it might also be necessary to run it with elevated privileges (at least I experienced this on a 2008 R2 domain controller). Granting the "Read all properties" right in AD, or sufficient rights, to the user running the script is a good idea. This can be done from AD Users and Computers, by clicking View -> Advanced Features (make sure there's a check mark next to it), then opening properties on the container(s)/user(s), selecting the Security tab and setting permissions as you see fit.
You might get a message about the user needing the "Log on as a batch job" privilege. This privilege can be set in the Group Policy Editor. To set it for a single server, run gpedit.msc and navigate to Computer Configuration -> Windows Settings -> Security Settings -> Local Policies -> User Rights Assignment. Here you can add the relevant user to the "Log on as a batch job" policy.Remember that you should specify the PowerShell executable as the file to run in Task Scheduler, and the script as the parameter. I recommend using the correct path to the desired PowerShell executable and the parameter '''-File "X:\path\to\Password-Expiration-Notifier.ps1"'''. Click here for an overview of the PowerShell executable file system locations on 32- and 64-bit Windows.
Set the "Start in" path to the path all the files are in, in Task Scheduler.If you need authentication, set the ''useDefaultCredentials'' value to '''true''' in the configuration file, and enter the username and password for an account in Task Scheduler. If you run it interactively, the current user's credentials are used.
Ok, I should have supported this somehow. Workaround: Look for this line in the script:
$private:users = Get-QADUser -SizeLimit 0 | Select-Object SamAccountName, GivenName, Sn, $config.emailAttribute, PasswordExpires
To make it only target users in a specific OU, change it to, for instance (notice the -SearchRoot parameter):
$private:users = Get-QADUser -SearchRoot ad.example.com/someOU/someSubOU -SizeLimit 0 | Select-Object SamAccountName, GivenName, Sn, $config.emailAttribute, PasswordExpires
$private:users = Get-QADUser -SearchRoot ad.example.com/someOU/someSubOU -SizeLimit 0 -IncludeAllProperties | Select-Object SamAccountName, GivenName, Sn, $config.emailAttribute, PasswordExpires
This article is now showing its age, and Get-ADUser from the Microsoft cmdlets is likely what people would use now. Also, the cmdlet Send-MailMessage would be handy, and is in PSv2. This could really be made pretty simple. The script does work fine as it is, though, and for people stuck with 2003 (R2) servers '''still''', the Quest cmdlets are manna from the heavens above.
I decided to implement it using a simple, custom configuration file format similar to an INI file. It's not a real INI file though; it's a custom format. Basically it's in the format ''key=value''. It seemed easier than a ton of parameters to the script specified on the command line.
Example ''pwdnot.conf'':smtp=smtp.getmail.no smtpPort=25 firstWarningDays=30 lastWarningDays=3 from=helpdesk@svendtech.no useDefaultCredentials=false enableSSL=true XXXemailAttribute=CustomAttribute42*'''smtp''': Outgoing SMTP server to be used.
There's an optional value called ''emailAttribute''. If you want to use a field different from the default ''Email'' LDAP/AD attribute field, you can specify for instance "CustomAttribute42" like this:
emailAttribute=CustomAttribute42
If you use a value in the config file that isn't expected, it will be ignored, so to "comment out" emailAttribute, you could use anything, such as "xxxemailAttribute=Pager", or whatever.
$errorLog = 'error.log' $mailSentLog = 'mail_sent.log' $mailErrorLog = 'mail_error.log' $firstWarnFile = 'first_warning.txt' $lastWarnFile = 'last_warning.txt' $configFile = 'pwdnot.conf'*'''error.log''': Contains all types of warnings and errors, except mail errors.
The actual script source code. I wrote this at an earlier stage of learning PowerShell, so I no longer agree on all the style issues, and the logic is pretty convoluted, so it could do with a rewrite, but it does work as it is.
<# .SYNOPSIS Svendsen Tech Password Expiration Notifier. Emails users a custom notification message the specified number of days before their passwords expire. .DESCRIPTION Edit pwdnot.conf to suit your environment. Edit last_warning.txt and first_warning.txt to contain the first and last warning mail. To only use one, just set the config file value for either the first or the last "days" to something that won't ever be triggered, like 500000. Please refer to the online Svendsen Tech PowerShell Wiki for further documentation: http://www.powershelladmin.com/wiki/Active_directory_password_expiration_notification.php Author: Joakim Svendsen, Svendsen Tech #> # Hard-coded file names. $errorLog = 'error.log' $mailSentLog = 'mail_sent.log' $mailErrorLog = 'mail_error.log' $firstWarnFile = 'first_warning.txt' $lastWarnFile = 'last_warning.txt' $configFile = 'pwdnot.conf' # Utility function that turns unquoted, space-separated strings into an array. # It means you can write "ql foo bar baz" rather than "@('foo', 'bar', 'baz')". function ql { $args } # Function to parse the configuration file. function Parse-Config { $config = @{} if (-not (Test-Path -PathType Leaf $configFile)) { Write-Host "${date}: Fatal error: File $configFile not found. Processing aborted." "${date}: Fatal error: File $configFile not found. Processing aborted." | Out-File -Append $errorLog exit 1 } Get-Content $configFile | Where-Object { $_ -match '\S' } | # Skip blank (whitespace only) lines. Foreach-Object { $key, $value = $_ -split '\s*=\s*'; $config.$key = $value } $requiredValues = ql smtp smtpPort from firstWarningDays lastWarningDays useDefaultCredentials enableSSL # Initialize this to false and exit after the loop if a required value is missing. [bool] $missingRequiredValue = $false foreach ($requiredValue in $requiredValues) { if (-not $config.ContainsKey($requiredValue)) { Write-Host "${date}: Error: Missing '$requiredValue'. Processing will be aborted." "${date}: Error: Missing '$requiredValue'. Processing will be aborted." | Out-File -Append $errorLog $missingRequiredValue = $true } } # Set the default e-mail address LDAP attribute/field unless specified (default: 'Email') if (-not $config.ContainsKey('emailAttribute')) { $config.'emailAttribute' = 'Email' } # Exit the program if a required value is missing in the configuration file. if ($missingRequiredValue) { exit 2 } $config } # Function to actually send the mail function Send-Warn-Mail { param( [string] $private:type, [string] $private:to, [string] $private:message, [string] $private:username ) if ($private:type -ieq 'first') { $private:subject = 'Your Password Will Expire In ' + $config.firstWarningDays + ' Days' } elseif ($private:type -ieq 'last') { $private:subject = 'Your Password Will Expire In ' + $config.lastWarningDays + ' Days!' } else { Write-Host "${date}: Wrong type (not first/last) received in Send-Warn-Mail for $private:to. Skipping." "${date}: Wrong type (not first/last). Error sending to $private:to" | Out-File -append $mailErrorLog return } $private:SmtpClient = New-Object System.Net.Mail.SmtpClient($config.smtp, $config.smtpPort) # If useDefaultCredentials is set to true, change the property on the SMTP client object. # It is false by default. if ($config.useDefaultCredentials -imatch 'true') { $private:SmtpClient.UseDefaultCredentials = $true } # If enableSSL is set to "true" in the config file, enable SSL. if ($config.enableSSL -imatch 'true') { $private:SmtpClient.enableSSL = $true } # Send the mail $private:SmtpClient.Send($config.from, $private:to, $private:subject, $private:message) if ( $? ) { "${date}: Sent $private:type warning mail to $private:to (Username: $private:username)" | Out-File -Append $mailSentLog } else { "${date}: Error sending $private:type warning mail to $private:to (Username: $private:username): " + $error[0].ToString() | Out-File -Append $mailErrorLog } # "Clear" the object/variable. Seems necessary. $private:SmtpClient = $null } # Simple function to get the number of days until the passwords expire. function Get-Pwd-Days { param([datetime] $private:pwdExpires) ($private:pwdExpires - $(Get-Date)).Days } # Function to iterate the first and last user sets and call the mail sending function. Function Iterate-Users { Param([Parameter(Mandatory=$true)][string] $private:type, [Parameter(Mandatory=$true)] $private:iterateUsers) foreach ($user in $private:iterateUsers) { if (-not $user.($config.emailAttribute)) { Write-Host "${date}: Error: No value for email address (LDAP: $($config.emailAttribute)) for '" $user.SamAccountName ` "'. Name:" $user.GivenName $user.Sn "${date}: Error: No email address (LDAP: $($config.emailAttribute)) for '" + $user.SamAccountName + ` "'. Name: " + $user.GivenName + ' ' + $user.Sn | Out-File -Append $mailErrorLog continue } $private:mailGreeting = 'Dear ' + $user.GivenName +' '+ $user.Sn + ' (username: ' + $user.SamAccountName.ToLower() + ").`n`n" $private:to = ($user.($config.emailAttribute)).ToLower() if ($private:type -ieq 'first') { $private:message = "$private:mailGreeting`n" + ($firstWarnText -join "`n") } else { $private:message = "$private:mailGreeting`n" + ($lastWarnText -join "`n") } Send-Warn-Mail $private:type $private:to $private:message $user.SamAccountName.ToLower() } }Powershell Windows AD All Categories######### END OF FUNCTIONS ##########
# Get current year, month and day for later use. $date = Get-Date -uformat %Y-%m-%d # Parse the configuration file (very little sanity checking). $config = Parse-Config $configFile # Set this to true if a file isn't found, and exit after the foreach [bool] $private:notFound = $false foreach ($private:file in @($firstWarnFile, $lastWarnFile)) { if ( ! (Test-Path $private:file) ) { Write-Host ${date}": Fatal error: Did not find $private:file." "${date}: Fatal error: Did not find $private:file." | Out-File -Append $errorLog $private:notFound = $true } } if ($private:notFound) { Write-Host ${date}": Fatal error: See $private:errorLog" exit 3 } # We know that these exist from earlier verification. $firstWarnText = Get-Content $firstWarnFile $lastWarnText = Get-Content $lastWarnFileAdd-PSSnapin Quest.ActiveRoles.AdManagement
$private:users = Get-QADUser -SizeLimit 0 | Select-Object SamAccountName, GivenName, Sn, $config.emailAttribute, PasswordExpires # Get users whose passwords will expire in the given amounts of days $private:firstWarnUsers = $private:users | Where-Object { $_.PasswordExpires -and ((Get-Pwd-Days $_.PasswordExpires) -eq $config.firstWarningDays) } $private:lastWarnUsers = $private:users | Where-Object { $_.PasswordExpires -and ((Get-Pwd-Days $_.PasswordExpires) -eq $config.lastWarningDays ) } <# Debug '';'';'First:';'';'' $private:firstWarnUsers '';'';'Last:';'';'' $private:lastWarnUsers #> if ($private:firstWarnUsers) { Iterate-Users 'first' $private:firstWarnUsers } if ($private:lastWarnUsers) { Iterate-Users 'last' $private:lastWarnUsers }
Minimum cookies is the standard setting. This website uses Google Analytics and Google Ads, and these products may set cookies. By continuing to use this website, you accept this.