PowerShell: Sending password expiration notices via GMail – Part 3

In Part 1 of this series, I showed you how to identify users whose password was about to expire. Then in Part 2 of the series, I took that list of users and sent email to them using gmail-hosted company email. This third part of the series pulls all that together into a single script, complete with comment-based help. As always, this and all my scripts are copyrighted, but you're welcome to use them as the basis for your own scripts. However, I do appreciate attribution. Thanks, and enjoy.

Sends a "Password Expiring" warning email through TreyResearch's gmail. 
Send-TreyPasswordExpiryNotice first creates a list of accounts whose password will expire in the 
near future (default is 1 week). It then emails the users to warn them that their password will expire soon. 

This initial version runs interactively only. 
Sends a warning notice to all TreyResearch users whose password will expire in the next 7 days or less.
Send-TreyPasswordExpiryNotice -Logging
Sends a warning notice to all TreyResearch users whose password will expire in the next 7 days or less, and 
creates a log file that is echoed to the console at the end. 
Send-TreyPasswordExpiryNotice -DaysWarning 14
Sends a warning notice to all TreyResearch users whose password will expire in the next 14 days or less.
Send-TreyPasswordExpiryNotice -DaysWarning 5 -Logging -Testing -Verbose
Does NOT send a warning notice to TreyResearch users, but rather processes the first user and sends a notice
to the admin user(s) and writes to the log file. The -Verbose switch will make it additionally chatty.
.Parameter DaysWarning
The number of days advanced warning to give users whose passwords are close to expiration. 
The default is 7 days or less. 
.Parameter Logging
Switch to enable logging. Logs are written to C:\Temp\emaillogs.csv. When this switch is true, 
Send-TreyPasswordExpiryNotice outputs a table with a list of accounts due to expire as well as 
writing to a log file. 
.Parameter Testing
Switch to enable testing. When enabled, email is sent to a list of Admin users and only a single account is processed. 
    Author: Charlie Russel
  ThanksTo: Robert Pearman (WSSMB MVP),Jeffrey Hicks (PS MVP)
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 06 Sept, 2016 (cpr)
          : 09 Dec,  2016 (cpr) -(Ver 1.5) -- Reworked: Only process users who need reminding. Formatting changes
     $DaysWarning = 7, 

#Set parameters for gmail.
$smtpServer  =""
$SMTPPort    = 587
$from        = "IT Notification <>"
$AdminUser1  = ""
$AdminUser2  = ""
$AdminUser3  = ""
$externalUser= ""

# Cast this to a list of strings to allow for multiple test recipients
[string[]]$testRecipient = $AdminUser1,$AdminUser2,$AdminUser3

 This uses a stored password sitting on a local hard drive. This is a reasonably
 secure way to work with passwords in a file, and is ONLY accessible by the user that created 
 it. Create the password with: 
   PSH> Read-Host -AsSecureString | ConvertFrom-SecureString | Out-File $home\Documents\TreyPW.txt

 See blog post at: for
 full details. 

 Alternately, simply prompt for the credentials here with Get-Credential.


$TreyUsr = ""
$TreyPW = Get-Content $Home\Documents\TreyPW.txt | ConvertTo-SecureString
$Cred = New-Object System.Management.Automation.PSCredential -ArgumentList $TreyUsr, $TreyPW

# Check Logging Settings 
if ($Logging) { 
   $logFile = "C:\Temp\emaillogs.csv"
   if (! (Test-Path "C:\Temp") ) {
      Write-Verbose "No C:\Temp directory, so creating one..."
      New-Item -Path "C:\" -Name Temp -ItemType Directory

    # Remove Logfile if it already exists
    If ( (Test-Path $logFile)) { 
      Remove-Item $logFile 
    # Create CSV File and Headers 
    New-Item -Path $logfile -ItemType File 
    Add-Content $logfile "Date,Name,EmailAddress,DaysLeft,ExpiresOn,Notified" 

# System Settings 
$textEncoding = [System.Text.Encoding]::UTF8 
$date = Get-Date -format "MM/dd/yyyy"

# Explicitly import the Active Directory module, but get rid of the noise if it's already loaded. 
Import-Module ActiveDirectory 4>$NULL

# Use the following to query the domain for who the PDC Emulator role holder is. 
$TreyDC = (Get-ADDomain -Identity "" -Credential $Cred).PDCEmulator

# Send a cc: to myself or a list of users
$AdminUser = ""
$cclist = @($AdminUser)

# Do calculations outside the ForEach loop whenever possible
$maxPasswordAge = (Get-ADDefaultDomainPasswordPolicy -Server $TreyDC -Credential $Cred).MaxPasswordAge
$today = (get-date) 

# Notice this doesn't get Expired or NeverExpires users. Don't want to send them emails.  
$TreyUsers = Get-ADUser -filter * `
                    -properties Name,PasswordNeverExpires,PasswordExpired,PasswordLastSet,EmailAddress `
                    -Server $TreyDC `
                    -Credential $Cred `
         | where { $_.Enabled -eq $True `
             -AND  $_.PasswordNeverExpires -eq $False `
             -AND  $_.passwordexpired -eq $False `
             -AND  $_.EMailAddress `
             -AND  (($today - $_.PasswordLastSet).Days -ge ($MaxPasswordAge.Days - $DaysWarning))
<# Get notification credentials. Prompt with Get-Credential if not using stored creds. 
$gCred = Get-Credential -UserName "" `
                        -Message  "Enter Password for IT-Notification account"
$gUsr = ""
$gPW = Get-Content "$Home\Documents\itnotificationsPW.txt" | ConvertTo-SecureString
$gCred = New-Object System.Management.Automation.PSCredential -ArgumentList $gUsr, $gPW

# Now, we start to do the work. 
foreach ($user in $TreyUsers) { 
    Write-Verbose "Processing user $user"
    $Name = $user.Name 
    $Email = $user.emailaddress 
    $SAM = $user.SAMAccountName
    $sent = " " 
    $passwordSetDate = $user.PasswordLastSet 
    Write-Verbose "$SAM last set their password on $PasswordSetDate"

    $expiresOn = $passwordSetDate + $maxPasswordAge 
    $DaysLeft = (New-TimeSpan -Start $today -End $Expireson).Days 
    if (($DaysLeft) -gt "1") { 
        $MessageDays = "in " + "$DaysLeft" + " days." 
    } else { 
        $MessageDays = "today!" 
    # Email Subject Set Here 
    $subject="Your password will expire $messageDays" 
    Write-Verbose "$Name`'s password will expire $messageDays"
    # Email Body Set Here, Note You can use HTML, including Images. 
    # This uses PowerShell's here-string. 
$body =@" 
Dear $name, 
<p>Your Active Directory Domain credentials <b>will expire $messagedays</b> 
Please update your credentials as soon as possible! <br> </p>
<p>If you are using a Windows domain joined system and are connected to the intranet, 
press ctrl-alt-delete and select change password. Alternatively, if you are outside of the 
network, connect to the corporate VPN and reset your password with the same process.<br> </p>
<p>If you are not using a Windows based system, ensure you are on the intranet or connected to 
the corporate VPN.  Proceed to <> 
and reset your password.<br> </p>
<p>This process will also sync your newly created AD password to your Gmail password. Please 
allow up to 5 minutes for replication of the passwords to occur.<br><br> </p>
<p><br><b>Problems</b>? <br>Please open a Service Desk request by clicking on the 
Help Agent icon on your system. If you are NOT running a Help Agent, please contact a member
of the IT Team for instructions on how to install the agent. It is a strict TreyResearch 
company policy that all company-owned systems run the Help Agent. <br></p>
<p>Thanks, <br>  
IT Team

    # If Testing Is Enabled - Email Administrator 
    if ($testing) { 
        $email = $testRecipient 
        $Subject = "PasswordExpiration Test Message"
        $cclist = $AdminUser2,$externalUser

   # Send Email Message 
    Write-Verbose "$SAM's password is due to expire in $DaysLeft which is less than the "
    Write-Verbose "DaysWarning Parameter setting of $DaysWarning days."

    # I've left this as a straight output to the host. If you want it quieter, make it a Write-Verbose
    "Sending Email Message to $email using $gUsr account"
    Send-Mailmessage -smtpServer $smtpServer `
                     -from $from `
                     -to $email `
                     -cc $cclist `
                     -subject $subject `
                     -body $body `
                     -bodyasHTML `
                     -priority High `
                     -Encoding $textEncoding `
                     -UseSSL `
                     -port $SMTPPort `
                     -Credential $gCred 
    $sent = "Yes"  # Used for logging
    if ($Logging) {
        Add-Content $logfile "$date,$Name,$email,$DaysLeft,$expiresOn,$Sent"  
   if ($Testing) {
       "Sleeping 5, then breaking so we only process a single record"
       Sleep 5

If ($Logging) { 
   # Use the logging file to display a table of the accounts about to expire. 
   $expiringAccts = Import-Csv -Path $logfile
   $expiringAccts | Sort-Object -Property ExpiresOn `
                  | Format-Table -AutoSize `
                           width=7}, `
                    @{Expression={(Get-Date -Date $_.ExpiresOn -Format 'MMMM dd')};`
                           Label="Expires On:"}

ETA: Minor bug fix (-Identity instead of -Identify. Sheesh!)

Remote Desktop Protocol Vulnerability

I don’t usually bother to write about Microsoft patches. After all, they happen every month, and we all are in the habit of patching (updating) moderately regularly. However, the update in MS12-020, described in this post, is both serious, and very likely to be a target in the very near future, though there aren’t any known exploits. Yet.

First, download and install the patch. That’s the obvious fix. However, if there is some particular reason why you can’t install the patch immediately, then you should configure all clients and servers that have Remote Desktop enabled to require Network Level Authentication (NLA). Microsoft has even made it easy for you. At the bottom of the TechNet post are links to apply a registry change to enforce Network Level Authentication. If you’ve got Windows XP or Windows Server 2003 clients, there’s even a Fix-It to turn on CredSSP so that they can connect using NLA.

Finally, you can configure group policy to require NLA on your network (and really, isn’t it about time you did?) Instructions are here:

I know I was lazy about it for a long time, even after pretty much all my downlevel clients were long gone. But now that GPO is set and enabled. And my SBS WSUS server is sending that patch to every computer in my network. No playing around with this one, folks.