{"id":4410,"date":"2022-10-31T10:59:27","date_gmt":"2022-10-31T17:59:27","guid":{"rendered":"https:\/\/SUMMALAI.COM\/?p=4410"},"modified":"2022-10-31T11:07:41","modified_gmt":"2022-10-31T18:07:41","slug":"how-to-setup-a-password-expiration-notification-via-emails","status":"publish","type":"post","link":"https:\/\/SUMMALAI.COM\/?p=4410","title":{"rendered":"How to Setup a Password Expiration Notification via Emails"},"content":{"rendered":"\n<p>Quick reference, Powershell script is available to download from here. You need to change its file type from .txt to .Ps1<\/p>\n\n\n\n<div class=\"wp-block-file\"><a id=\"wp-block-file--media-9896601e-6aa5-45d4-a928-2468d9b275fe\" href=\"https:\/\/SUMMALAI.COM\/wp-content\/uploads\/2022\/10\/Password-Expire-Email_Public-1.txt\">Password-Expire-Email_Public-1<\/a><a href=\"https:\/\/SUMMALAI.COM\/wp-content\/uploads\/2022\/10\/Password-Expire-Email_Public-1.txt\" class=\"wp-block-file__button\" download aria-describedby=\"wp-block-file--media-9896601e-6aa5-45d4-a928-2468d9b275fe\">Download<\/a><\/div>\n\n\n\n<p>Have you ever had a need to configure notifications for user&#8217;s password expirations but found that existing solutions didn&#8217;t quite fit the bill? We all know you can use built-in solutions with Windows and Active Directory\/Group Policy but this requires users to interactively log-on to a network-based computer. What about those&nbsp;BYOD or mobile users or&nbsp;users of web apps\/email? More often than not, these users will have to call the helpdesk because they had no idea their domain passwords were going to expire. Statistics show that some of the most common calls to the helpdesk are password-related and implementing a&nbsp;process like the one covered here could really make a dent in your helpdesk call volume&nbsp;and costs.<\/p>\n\n\n\n<p>Recently, a customer asked for some help implementing a solution for this issue based on a script they&#8217;d found on the Microsoft TechNet Script Center.&nbsp; The script&nbsp;queries the&nbsp;<strong>pwdLastSet&nbsp;<\/strong>attribute of user accounts in AD and the&nbsp;<strong>MaxPwdAge&nbsp;<\/strong>property within the domain, then does some time computations and sends an email to those users who are near a password expiration &#8216;event.&#8217;<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/techcommunity.microsoft.com\/t5\/image\/serverpage\/image-id\/52206i111BCF74ABB534B8\/image-size\/large?v=v2&amp;px=999\" alt=\"thumbnail image 1 of blog post titled \n\t\n\t\n\t \n\t\n\t\n\t\n\t\t\t\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tHow to Setup a Password Expiration Notification Email Solution\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\n\t\t\n\t\n\t\t\t\n\t\n\t\n\t\n\t\n\t\n\"\/><\/figure>\n\n\n\n<p>I thought it would make a helpful blog post to cover some of the details and considerations when implementing a solution like this.<\/p>\n\n\n\n<p>RAW Scripts here for reference.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#################################################################################################################\r\n#\r\n# Password-Expiration-Notifications v20221028\r\n# Last modified: By Summa Lai\r\n# Originally from v1.4 @ https:\/\/gallery.technet.microsoft.com\/Password-Expiry-Email-177c3e27\r\n# Script to Automated Email Reminders when Users Passwords due to Expire.\r\n#\r\n# Requires: Windows PowerShell Module for Active Directory\r\n#\r\n##################################################################################################################\r\n# Please Configure the following variables....\r\n$testing = $true # Set to $false to Email Users. $true to email samples to administrators only (see $sampleEmails below)\r\n$SearchBase=\"DC=summalai,DC=com\"\r\n### PURGING this option; seems to cause issue. # $ExcludeList=\"'New Employees'|'Separated Employees'\"   #in the form of \"SubOU1|SubOU2|SubOU3\" -- possibly needing single quote for OU's with spaces, separate OU's with pipe and double-quote the list.\r\n\r\n&#91;System.Net.ServicePointManager]::SecurityProtocol = &#91;System.Net.SecurityProtocolType]::Tls12;\r\n\r\n$expireindays = 14 #number of days of soon-to-expire paswords. i.e. notify for expiring in X days (and every day until $negativedays)\r\n$negativedays = -3 #negative number of days (days already-expired). i.e. notify for expired X days ago\r\n$from = \"Password Administrator &lt;Do-Not-Reply@summalai.com>\"\r\n$logging = $true # Set to $false to Disable Logging\r\n$logNonExpiring = $false\r\n$logFile = \"c:\\temp\\PS-pwd-expiry.csv\" # ie. c:\\mylog.csv\r\n$adminEmailAddr = \"Summa.Lai@summalai.com\"\r\n# $adminEmailAddr = \"help@summalai.com\",\"Summa.Lai@summalai.com\",\"support@summalai.com\" #multiple addr allowed but MUST be independent strings separated by comma\r\n$sampleEmails = 20 #number of sample email to send to adminEmailAddr when testing ; in the form $sampleEmails=\"ALL\" or $sampleEmails=&#91;0..X] e.g. $sampleEmails=0 or $sampleEmails=3 or $sampleEmails=\"all\" are all valid.\r\n$smtpCredentialsUsername = \"Do-Not-Reply@summalai.com\"\r\n$smtpCredentialsPassword = ConvertTo-SecureString -String \"PASSWORD-HERE\" -AsPlainText -Force\r\n$smtpServer = \"smtp.office365.com\"\r\n$Creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $smtpCredentialsUsername, $smtpCredentialsPassword\r\n# please edit $body variable within the code\r\n###################################################################################################################\r\n\r\n\r\n# System Settings\r\n$textEncoding = &#91;System.Text.Encoding]::UTF8\r\n$date = Get-Date -format yyyy-MM-dd #for logfile only\r\n$starttime=Get-Date #need time also; don't use date from above\r\n\r\nWrite-Host \"Processing `\"$SearchBase`\" for Password-Expiration-Notifications\"\r\nWrite-Host \"Testing Mode: $testing\"\r\n\r\n# Get Users From AD who are Enabled, Passwords Expire\r\nImport-Module ActiveDirectory\r\nWrite-Host \"Gathering User List\"\r\n$users = get-aduser -SearchBase $SearchBase -Filter {(enabled -eq $true) -and (passwordNeverExpires -eq $false)} -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated\r\nWrite-Host \"Filtering User List\"\r\n### PURGING this option; seems to cause issue. # $users = $users | Where-Object {$_.DistinguishedName -notlike $ExcludeList}   ##also try -notmatch, needs heavy testing\r\n$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge\r\n\r\n$countprocessed=${users}.Count\r\n$samplesSent=0\r\n$countsent=0\r\n$countnotsent=0\r\n$countfailed=0\r\n$nonexpiring=0\r\n\r\nWrite-Host \"${countprocessed} user-accounts selected to iterate.\"\r\n\r\n#set max sampleEmails to send to $adminEmailAddr\r\nif ( $sampleEmails -isNot &#91;int]) {\r\n    if ( $sampleEmails.ToLower() -eq \"all\") {\r\n    $sampleEmails=$users.Count\r\n    } #else use the value given\r\n}\r\n\r\nif (($testing -eq $true) -and ($sampleEmails -ge 0)) {\r\n    Write-Host \"Testing only; $sampleEmails email samples will be sent to $adminEmailAddr\"\r\n} elseif (($testing -eq $true) -and ($sampleEmails -eq 0)) {\r\n    Write-Host \"Testing only; emails will NOT be sent\"\r\n}\r\n\r\n# Create CSV Log\r\nif ($logging -eq $true) {\r\n    #Always purge old CSV file\r\n    Out-File $logfile\r\n    Add-Content $logfile \"`\"Date`\",`\"SAMAccountName`\",`\"DisplayName`\",`\"Created`\",`\"PasswordSet`\",`\"DaystoExpire`\",`\"ExpiresOn`\",`\"EmailAddress`\",`\"Notified`\"\"\r\n}\r\n\r\n# Process Each User for Password Expiry\r\nforeach ($user in $users) {\r\n    $dName = $user.displayName\r\n    $sName = $user.sAMAccountName\r\n    $emailaddress = $user.emailaddress\r\n    $whencreated = $user.whencreated\r\n    $passwordSetDate = $user.PasswordLastSet\r\n    $sent = \"\" # Reset Sent Flag\r\n\r\n    $PasswordPol = (Get-AduserResultantPasswordPolicy $user)\r\n    # Check for Fine Grained Password\r\n    if (($PasswordPol) -ne $null) {\r\n        $maxPasswordAge = ($PasswordPol).MaxPasswordAge\r\n    } else {\r\n        # No FGPP set to Domain Default\r\n        $maxPasswordAge = $DefaultmaxPasswordAge\r\n    }\r\n\r\n    #If maxPasswordAge=0 then same as passwordNeverExpires, but PasswordCannotExpire bit is not set\r\n    if ($maxPasswordAge -eq 0) {\r\n        Write-Host \"$sName : MaxPasswordAge = $maxPasswordAge (i.e. PasswordNeverExpires) but bit not set -- User not selected to receive email.\"\r\n    }\r\n\r\n    $expiresOn = $passwordsetdate + $maxPasswordAge\r\n    $today = (get-date)\r\n\r\n    if ( ($user.passwordexpired -eq $false) -and ($maxPasswordAge -ne 0) ) {   #not Expired and not PasswordNeverExpires\r\n        $daystoexpire = (New-TimeSpan -Start $today -End $expiresOn).Days\r\n    } elseif ( ($user.passwordexpired -eq $true) -and ($passwordSetDate -ne $null) -and ($maxPasswordAge -ne 0) ) {   #if expired and passwordSetDate exists and not PasswordNeverExpires\r\n        # i.e. already expired\r\n        $daystoexpire = -((New-TimeSpan -Start $expiresOn -End $today).Days)\r\n    } else {\r\n        # i.e. (passwordSetDate = never) OR (maxPasswordAge = 0)\r\n        $daystoexpire=\"NA\"\r\n        $nonexpiring += 1\r\n        #continue #\"continue\" would skip user, but bypass any non-expiry logging\r\n    }\r\n\r\n    #Write-Host \"$sName : DaysToExpire: $daystoexpire MaxPasswordAge: $maxPasswordAge\" #debug\r\n\r\n    # Set verbiage based on Number of Days to Expiry.\r\n    Switch ($daystoexpire) {\r\n        {$_ -ge $negativedays -and $_ -le \"-1\"} {$messageDays = \"has expired\"}\r\n        \"0\" {$messageDays = \"will expire today\"}\r\n        \"1\" {$messageDays = \"will expire in 1 day\"}\r\n        default {$messageDays = \"will expire in \" + \"$daystoexpire\" + \" days\"}\r\n    }\r\n\r\n    # Email Subject Set Here\r\n    $subject=\"Your password $messageDays\"\r\n\r\n    # Email Body Set Here, Note You can use HTML, including Images.\r\n    $body=\"\r\n    &lt;p>Your password for your &lt;b>$emailaddress&lt;\/b> account $messageDays.  After expired, you will not be able to login until your password is changed.&lt;\/p>\r\n\r\n    &lt;p>Please visit https:\/\/portal.summalai.com to change your password. &lt;\/p>\r\n    &lt;p>Password requirements:&lt;br>\r\n         \u2022\tMust be a minimum of 16 characters.&lt;br>\r\n         \u2022\tMust include letters, numbers, and special characters.&lt;br>\r\n         \u2022\tMust not include first or last name.&lt;br>\r\n         \u2022\tMust not be a password that you have used in the past.&lt;br>\r\n         \u2022\tMust be UNIQUE and not used on any other system.&lt;\/p>\r\n    &lt;p>Once you have changed your password, please immediately log off all sessions including your laptop.  You will be asked to login to your laptop and email accounts with your new password.&lt;\/p>\r\n    &lt;p>If you have any questions, please contact helpdesk or Summa Lai.&lt;\/p>\r\n\r\n    Thank you!&lt;br>\r\n    help@summalai.com&lt;br>\r\n    summa.lai@summalai.com&lt;br>\r\n    &lt;\/p>\r\n    \"\r\n\r\n    # If testing-enabled and send-samples, then set recipient to adminEmailAddr else user's EmailAddress\r\n    if (($testing -eq $true) -and ($samplesSent -le $sampleEmails)) {\r\n        $recipient = $adminEmailAddr\r\n    } else {\r\n        $recipient = $emailaddress\r\n    }\r\n\r\n    #if in trigger range, send email\r\n    if ( ($daystoexpire -ge $negativedays) -and ($daystoexpire -le $expireindays) -and ($daystoexpire -ne \"NA\") ) {\r\n        Write-Host \"$sName : Selected to receive email: password ${messageDays}\"\r\n        # Send Email Message\r\n        if (($emailaddress) -ne $null) {\r\n            if ( ($testing -eq $false) -or (($testing -eq $true) -and ($samplesSent -lt $sampleEmails)) ) {\r\n                try {\r\n                    Send-Mailmessage -from $from -to $recipient -subject $subject -smtpServer $smtpServer -Credential $creds -UseSsl -Port 587 -body $body -bodyasHTML -priority High -Encoding $textEncoding -ErrorAction Stop -ErrorVariable err \r\n               } catch {\r\n                    write-host \"Error: Could not send email to $recipient via $smtpServer\"\r\n                    $sent = \"Send fail\"\r\n                    $countfailed++\r\n                } finally {\r\n                    if ($err.Count -eq 0) {\r\n                        write-host \"Sent email for $sName to $recipient\"\r\n                        $countsent++\r\n                        if ($testing -eq $true) {\r\n                            $samplesSent++\r\n                            $sent = \"toAdmin\"\r\n                        } else { $sent = \"Yes\" }\r\n                    }\r\n                }\r\n            } else {\r\n                Write-Host \"Testing mode: skipping email to $recipient\"\r\n                $sent = \"No\"\r\n                $countnotsent++\r\n            }\r\n        } else {\r\n            Write-Host \"$dName ($sName) has no email address.\"\r\n            $sent = \"No addr\"\r\n            $countnotsent++\r\n        }\r\n\r\n        # If Logging is Enabled Log Details\r\n        if ($logging -eq $true) {\r\n            Add-Content $logfile \"`\"$date`\",`\"$sName`\",`\"$dName`\",`\"$whencreated`\",`\"$passwordSetDate`\",`\"$daystoExpire`\",`\"$expireson`\",`\"$emailaddress`\",`\"$sent`\"\"\r\n        }\r\n    } else {\r\n        #if ( ($daystoexpire -eq \"NA\") -and ($maxPasswordAge -eq 0) ) { Write-Host \"$sName PasswordNeverExpires\" } elseif ($daystoexpire -eq \"NA\") { Write-Host \"$sName PasswordNeverSet\" } #debug\r\n        # Log Non Expiring Password\r\n        if ( ($logging -eq $true) -and ($logNonExpiring -eq $true) ) {\r\n            if ($maxPasswordAge -eq 0 ) {\r\n                $sent = \"NeverExp\"\r\n            } else {\r\n                $sent = \"No\"\r\n            }\r\n            Add-Content $logfile \"`\"$date`\",`\"$sName`\",`\"$dName`\",`\"$whencreated`\",`\"$passwordSetDate`\",`\"$daystoExpire`\",`\"$expireson`\",`\"$emailaddress`\",`\"$sent`\"\"\r\n        }\r\n    }\r\n\r\n} # End User Processing\r\n\r\n$endtime=Get-Date\r\n$totaltime=($endtime-$starttime).TotalSeconds\r\n$minutes=\"{0:N0}\" -f ($totaltime\/60)\r\n$seconds=\"{0:N0}\" -f ($totaltime%60)\r\n\r\nWrite-Host \"$countprocessed Users from `\"$SearchBase`\" Processed in $minutes minutes $seconds seconds.\"\r\nWrite-Host \"Email trigger range from $negativedays (past) to $expireindays (upcoming) days of user's password expiry date.\"\r\nWrite-Host \"$nonexpiring Non-Expiring accounts.\"\r\nWrite-Host \"$countsent Emails Sent.\"\r\nWrite-Host \"$countnotsent Emails skipped.\"\r\nWrite-Host \"$countfailed Emails failed.\"\r\n\r\n# sort the CSV file\r\nif ($logging -eq $true) {\r\n    Rename-Item $logfile \"$logfile.old\"\r\n    import-csv \"$logfile.old\" | sort ExpiresOn | export-csv $logfile -NoTypeInformation\r\n    Remove-Item \"$logFile.old\"\r\n    Write-Host \"CSV File created at ${logfile}.\"\r\n\r\n    if ($testing -eq $true) {\r\n        $body=\"&lt;b>&lt;i>Testing Mode.&lt;\/i>&lt;\/b>&lt;br>\"\r\n    } else {\r\n        $body=\"\"\r\n    }\r\n\r\n    $body+=\"\r\n    CSV Attached for $date&lt;br>\r\n    $countprocessed Users from `\"$SearchBase`\" Processed in $minutes minutes $seconds seconds.&lt;br>\r\n    Email trigger range from $negativedays (past) to $expireindays (upcoming) days of user's password expiry date.&lt;br>\r\n    $nonexpiring Non-Expiring accounts.&lt;br>\r\n    $countsent Emails Sent.&lt;br>\r\n    $countnotsent Emails skipped.&lt;br>\r\n    $countfailed Emails failed.\r\n    \"\r\n\r\n\r\n    try {\r\n        #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\r\n        Send-Mailmessage -from $from -to $adminEmailAddr -subject \"Password Expiry Logs\" -smtpServer $smtpServer -Credential $creds -UseSsl -Port 587 -body $body -bodyasHTML -Attachments \"$logFile\" -priority High -Encoding $textEncoding -ErrorAction Stop -ErrorVariable err\r\n\t} catch {\r\n         write-host \"Error: Failed to email CSV log to $adminEmailAddr via $smtpServer\"\r\n    } finally {\r\n        if ($err.Count -eq 0) {\r\n            write-host \"CSV emailed to $adminEmailAddr\"\r\n        }\r\n    }\r\n}\r\n\r\n# End<\/code><\/pre>\n\n\n\n<p>Ref: <a href=\"https:\/\/techcommunity.microsoft.com\/t5\/core-infrastructure-and-security\/how-to-setup-a-password-expiration-notification-email-solution\/ba-p\/257836\">How to Setup a Password Expiration Notification Email Solution &#8211; Microsoft Community Hub<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Quick reference, Powershell script is available to download from here. You need to change its file type from .txt to .Ps1 Have you ever had a need to configure notifications for user&#8217;s password expirations but found that existing solutions didn&#8217;t quite fit the bill? We all know you can use built-in solutions with Windows and <a class=\"read-more\" href=\"https:\/\/SUMMALAI.COM\/?p=4410\">Read More<\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[10,1224,14,15],"tags":[1363,1362],"class_list":["post-4410","post","type-post","status-publish","format-standard","hentry","category-microsoft","category-powershell","category-windows-7-8-10","category-windows-servers","tag-password-expiration-notification-by-email","tag-password-expiration-notification-via-email"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=\/wp\/v2\/posts\/4410","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4410"}],"version-history":[{"count":2,"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=\/wp\/v2\/posts\/4410\/revisions"}],"predecessor-version":[{"id":4416,"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=\/wp\/v2\/posts\/4410\/revisions\/4416"}],"wp:attachment":[{"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4410"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4410"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/SUMMALAI.COM\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4410"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}