Script Repository


Notify users whose passwords are to expire after a specific number of days

March 18, 2021
347

The script sends email notifications to users whose password is to expire in a predefined number of days. To execute the script, create a scheduled task configured for the Domain-DNS object type and add a managed domain to the Activity Scope.

Parameters:

  • $days - Specifies amounts of days users passwords should expire in for the script to send them emails.
  • $messageTemplate - Specifies a template for the email notification. In the template, the {0} placeholder will be replaced with the number of days before the recipient password expiration.
  • $subjectTemplate - Specifies a template for the email notification subject. In the template, the {0} placeholder will be replaced with the number of days before the recipient password expiration.
Edit Remove
PowerShell
$days = @(5,7,14) # TODO: modify me
$messageTemplate = "Your password will expire in {0} days" #TODO: modify me
$subjectTemplate = "Your password will expire in {0} days" # TODO: modify me

# Build search filter
# We filter out user accounts whose password never expires or must be changed at next logon,
# as well as interdomain trust accounts and accounts required to use smart cards
$filterUsers = "(&(sAMAccountType=805306368)(|(!(msExchRecipientTypeDetails=*))(!(msExchRecipientTypeDetails:1.2.840.113556.1.4.804:=7276219883574))))"
$filterPasswordLastSet = "(!(pwdLastSet=0))"
$filterPasswordNeverExpires = "(!(userAccountControl:1.2.840.113556.1.4.803:=65536))"
$filterSmartCardLogon = "(!(userAccountControl:1.2.840.113556.1.4.803:=262144))"
$filterInterdomainTrust = "(!(userAccountControl:1.2.840.113556.1.4.803:=2048))"
$filter = "(&" + $filterUsers + $filterPasswordLastSet + $filterPasswordNeverExpires +
    $filterSmartCardLogon + $filterInterdomainTrust + ")"

$searcher = New-Object Softerra.Adaxes.Adsi.Search.DirectorySearcher $NULL, $False
$searcher.SearchParameters.Filter = $filter
$searcher.VirtualRoot = $True
$searcher.SearchParameters.SearchScope = "ADS_SCOPE_SUBTREE"
$searcher.SearchParameters.ReferralChasing = "ADS_CHASE_REFERRALS_NEVER"
$searcher.SearchParameters.PageSize = 500
$searcher.SetPropertiesToLoad(@("msDS-UserPasswordExpiryTimeComputed", "msDS-ResultantPSO", "pwdLastSet", "mail"))

# Map domain names to maximum password age specified in the default domain password policy
$domainToMaxPwdAge = @{}

try
{
    # Execute search
    $searchResultIterator = $searcher.ExecuteSearch()
    $searchResults = $searchResultIterator.FetchAll()

    $now = Get-Date
    foreach ($searchResult in $searchResults)
    {
        $expiryTime = $searchResult.Properties["msDS-UserPasswordExpiryTimeComputed"].Value
        if ($NULL -eq $expiryTime) # Fine-Grained Password Policies not supported
        {
            # Get the maximum password age from the default domain password policy
            $domain = $Context.GetObjectDomain($searchResult.AdsPath.DN)
            if (-not $domainToMaxPwdAge.Contains($domain))
            {
                # Bind to the default naming context
                $nc = $Context.BindToObjectEx("Adaxes://$domain", $False)
                
                # Get maximum password age
                $maxPwdAgeLargeInt = $nc.Get("maxPwdAge")
                $domainToMaxPwdAge[$domain] =
                    [Softerra.Adaxes.Adsi.AdsLargeInteger]::ToInt64($maxPwdAgeLargeInt)
            }

            $maxPwdAge = $domainToMaxPwdAge[$domain]
            if ($maxPwdAge -eq 0x8000000000000000) # no maximum password age
            {
                continue
            }
            else
            {
                $passwordLastSet = $searchResult.Properties["pwdLastSet"].Value
                $expiryTime = $passwordLastSet - $maxPwdAge
            }
        }
        
        if ($expiryTime -eq 9223372036854775807)
        {
            continue # No password expiration specified
        }
        
        $expiresOnDate = [DateTime]::FromFiletime([Int64]::Parse($expiryTime))
        $timeSpanTillExpiration = New-TimeSpan -Start $now -End $expiresOnDate
        $pwdExpiresDaysLeft = $timeSpanTillExpiration.Days
        if ($days -notcontains $pwdExpiresDaysLeft)
        {
            continue
        }
        
        # Send mail
        $mail = $searchResult.Properties["mail"].Value
        if ([System.String]::IsNullOrEmpty($mail))
        {
            $userName = $Context.GetDisplayNameFromAdsPath($searchResult.AdsPath)
            $Context.LogMessage("User $userName does not have an email address.", "Warning")
        }
        else
        {
            $message = [System.String]::Format($messageTemplate, $pwdExpiresDaysLeft)
            $subject = [System.String]::Format($subjectTemplate, $pwdExpiresDaysLeft)
            $Context.SendMail($mail, $subject, $message, $NULL)
        }
    }
}
finally
{
    # Release resources
    if ($searchResultIterator) { $searchResultIterator.Dispose() }
}

Comments ( 3 )
avatar
Sandra Mitchell
Mar 18, 2021
I've got it. Thanks so much!!!
avatar
Sandra Mitchell
Mar 22, 2021
We have two managed domains. Do both need to be added to the Activity scope? If so, will this cause the script to execute twice (once for each domain). That would not be ideal, but doable if necessary. The way we've gotten around this scenario in the past was to put all users from both domains in a Business Unit and then iterate through the BU. Would that make more sense?
avatar
Support
Mar 23, 2021
Hello Sandra,

The scheduled task should be configured for the Domain-DNS object type and have only a single managed domain in the Activity Scope. The script will always search through all users in all managed domains. There is no need to add more than one managed domain to the Activity Scope as it will make the script run twice and thus all users will be notified twice.
Leave a comment