PSPasswordExpiryNotifications.psm1
function ConvertFrom-DistinguishedName { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER DistinguishedName Parameter description .PARAMETER ToOrganizationalUnit Parameter description .PARAMETER ToDC Parameter description .PARAMETER ToDomainCN Parameter description .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName Output: Przemyslaw Klys .NOTES General notes #> [CmdletBinding()] param( [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName, [switch] $ToOrganizationalUnit, [switch] $ToDC, [switch] $ToDomainCN ) process { foreach ($Distinguished in $DistinguishedName) { if ($ToDomainCN) { $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' $CN = $DN -replace ',DC=', '.' -replace "DC=" $CN } elseif ($ToOrganizationalUnit) { [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value } elseif ($ToDC) { #return [Regex]::Match($DistinguishedName, '(?=DC=)(.*\n?)(?<=.)').Value # return [Regex]::Match($DistinguishedName, '.*?(DC=.*)').Value $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' #return [Regex]::Match($DistinguishedName, 'CN=.*?(DC=.*)').Groups[1].Value } else { $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$' $Output = foreach ($_ in $Distinguished) { $_ -match $Regex $Matches } $Output.cn } } } } function Get-FileName { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Extension Parameter description .PARAMETER Temporary Parameter description .PARAMETER TemporaryFileOnly Parameter description .EXAMPLE Get-FileName -Temporary Output: 3ymsxvav.tmp .EXAMPLE Get-FileName -Temporary Output: C:\Users\pklys\AppData\Local\Temp\tmpD74C.tmp .EXAMPLE Get-FileName -Temporary -Extension 'xlsx' Output: C:\Users\pklys\AppData\Local\Temp\tmp45B6.xlsx .NOTES General notes #> [CmdletBinding()] param( [string] $Extension = 'tmp', [switch] $Temporary, [switch] $TemporaryFileOnly ) if ($Temporary) { return "$($([System.IO.Path]::GetTempFileName()).Replace('.tmp','')).$Extension" } if ($TemporaryFileOnly) { # Generates 3ymsxvav.tmp return "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension" } } function Get-HashMaxValue { [CmdletBinding()] param ( [Object] $hashTable, [switch] $Lowest ) if ($Lowest) { return ($hashTable.GetEnumerator() | Sort-Object value -Descending | Select-Object -Last 1).Value } else { return ($hashTable.GetEnumerator() | Sort-Object value -Descending | Select-Object -First 1).Value } } function Send-Email { [CmdletBinding(SupportsShouldProcess = $true)] param ( [alias('EmailParameters')][System.Collections.IDictionary] $Email, [string] $Body, [string[]] $Attachment, [System.Collections.IDictionary] $InlineAttachments, [string] $Subject, [string[]] $To, [PSCustomObject] $Logger ) try { # Following code makes sure both formats are accepted. if ($Email.EmailTo) { $EmailParameters = $Email.Clone() $EmailParameters.EmailEncoding = $EmailParameters.EmailEncoding -replace "-", '' $EmailParameters.EmailEncodingSubject = $EmailParameters.EmailEncodingSubject -replace "-", '' $EmailParameters.EmailEncodingBody = $EmailParameters.EmailEncodingSubject -replace "-", '' $EmailParameters.EmailEncodingAlternateView = $EmailParameters.EmailEncodingAlternateView -replace "-", '' } else { $EmailParameters = @{ EmailFrom = $Email.From EmailTo = $Email.To EmailCC = $Email.CC EmailBCC = $Email.BCC EmailReplyTo = $Email.ReplyTo EmailServer = $Email.Server EmailServerPassword = $Email.Password EmailServerPasswordAsSecure = $Email.PasswordAsSecure EmailServerPasswordFromFile = $Email.PasswordFromFile EmailServerPort = $Email.Port EmailServerLogin = $Email.Login EmailServerEnableSSL = $Email.EnableSsl EmailEncoding = $Email.Encoding -replace "-", '' EmailEncodingSubject = $Email.EncodingSubject -replace "-", '' EmailEncodingBody = $Email.EncodingBody -replace "-", '' EmailEncodingAlternateView = $Email.EncodingAlternateView -replace "-", '' EmailSubject = $Email.Subject EmailPriority = $Email.Priority EmailDeliveryNotifications = $Email.DeliveryNotifications EmailUseDefaultCredentials = $Email.UseDefaultCredentials # EmailAlternativeClient = $Email.AlternativeClient } } } catch { return @{ Status = $False Error = $($_.Exception.Message) SentTo = '' } } $SmtpClient = [System.Net.Mail.SmtpClient]::new() if ($EmailParameters.EmailServer) { $SmtpClient.Host = $EmailParameters.EmailServer } else { return @{ Status = $False Error = "Email Server Host is not set." SentTo = '' } } # Adding parameters to login to server if ($EmailParameters.EmailServerPort) { $SmtpClient.Port = $EmailParameters.EmailServerPort } else { return @{ Status = $False Error = "Email Server Port is not set." SentTo = '' } } if ($EmailParameters.EmailServerLogin) { $Credentials = Request-Credentials -UserName $EmailParameters.EmailServerLogin ` -Password $EmailParameters.EmailServerPassword ` -AsSecure:$EmailParameters.EmailServerPasswordAsSecure ` -FromFile:$EmailParameters.EmailServerPasswordFromFile ` -NetworkCredentials #-Verbose $SmtpClient.Credentials = $Credentials } if ($EmailParameters.EmailServerEnableSSL) { $SmtpClient.EnableSsl = $EmailParameters.EmailServerEnableSSL } $MailMessage = [System.Net.Mail.MailMessage]::new() $MailMessage.From = $EmailParameters.EmailFrom if ($To) { foreach ($T in $To) { $MailMessage.To.add($($T)) } } else { if ($EmailParameters.Emailto) { foreach ($To in $EmailParameters.Emailto) { $MailMessage.To.add($($To)) } } } if ($EmailParameters.EmailCC) { foreach ($CC in $EmailParameters.EmailCC) { $MailMessage.CC.add($($CC)) } } if ($EmailParameters.EmailBCC) { foreach ($BCC in $EmailParameters.EmailBCC) { $MailMessage.BCC.add($($BCC)) } } if ($EmailParameters.EmailReplyTo) { $MailMessage.ReplyTo = $EmailParameters.EmailReplyTo } $MailMessage.IsBodyHtml = $true if ($Subject -eq '') { $MailMessage.Subject = $EmailParameters.EmailSubject } else { $MailMessage.Subject = $Subject } $MailMessage.Priority = [System.Net.Mail.MailPriority]::$($EmailParameters.EmailPriority) # Encoding if ($EmailParameters.EmailEncodingSubject) { $MailMessage.SubjectEncoding = [System.Text.Encoding]::$($EmailParameters.EmailEncodingSubject) } elseif ($EmailParameters.EmailEncoding) { $MailMessage.SubjectEncoding = [System.Text.Encoding]::$($EmailParameters.EmailEncoding) } if ($EmailParameters.EmailEncodingBody) { $MailMessage.BodyEncoding = [System.Text.Encoding]::$($EmailParameters.EmailEncodingBody) } elseif ($EmailParameters.EmailEncoding) { $MailMessage.BodyEncoding = [System.Text.Encoding]::$($EmailParameters.EmailEncoding) } #$MailMessage.BodyTransferEncoding = [System.Net.Mime.TransferEncoding]::QuotedPrintable if ($EmailParameters.EmailUseDefaultCredentials) { $SmtpClient.UseDefaultCredentials = $EmailParameters.EmailUseDefaultCredentials } if ($EmailParameters.EmailDeliveryNotifications) { $MailMessage.DeliveryNotificationOptions = $EmailParameters.EmailDeliveryNotifications } # Inlining attachment (s) if ($PSBoundParameters.ContainsKey('InlineAttachments')) { # having any other encoding here caused Thunderbird to play weird things if ($EmailParameters.EmailEncodingAlternateView) { $BodyPart = [Net.Mail.AlternateView]::CreateAlternateViewFromString($Body, [System.Text.Encoding]::$($EmailParameters.EmailEncodingAlternateView) , 'text/html' ) } else { $BodyPart = [Net.Mail.AlternateView]::CreateAlternateViewFromString($Body, [System.Text.Encoding]::UTF8, 'text/html' ) } <# if ($EmailParameters.EmailEncodingBody) { $BodyPart = [Net.Mail.AlternateView]::CreateAlternateViewFromString($Body, [System.Text.Encoding]::$($EmailParameters.EmailEncodingBody), 'text/html' ) } elseif ($EmailParameters.EmailEncoding) { $BodyPart = [Net.Mail.AlternateView]::CreateAlternateViewFromString($Body, [System.Text.Encoding]::$($EmailParameters.EmailEncoding), 'text/html' ) } else { $BodyPart = [Net.Mail.AlternateView]::CreateAlternateViewFromString($Body, 'text/html' ) } #> #$BodyPart.TransferEncoding = [System.Net.Mime.TransferEncoding]::QuotedPrintable $MailMessage.AlternateViews.Add($BodyPart) foreach ($Entry in $InlineAttachments.GetEnumerator()) { try { $FilePath = $Entry.Value Write-Verbose $FilePath if ($Entry.Value.StartsWith('http')) { $FileName = $Entry.Value.Substring($Entry.Value.LastIndexOf("/") + 1) $FilePath = Join-Path $env:temp $FileName Invoke-WebRequest -Uri $Entry.Value -OutFile $FilePath } $ContentType = Get-MimeType -FileName $FilePath $InAttachment = [Net.Mail.LinkedResource]::new($FilePath, $ContentType ) $InAttachment.ContentId = $Entry.Key $BodyPart.LinkedResources.Add( $InAttachment ) } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Error "Error inlining attachments: $ErrorMessage" } } } else { $MailMessage.Body = $Body } # Attaching file (s) if ($PSBoundParameters.ContainsKey('Attachment')) { foreach ($Attach in $Attachment) { if (Test-Path -LiteralPath $Attach) { try { $File = [Net.Mail.Attachment]::new($Attach) #Write-Verbose "Send-Email - Attaching file $Attach" $MailMessage.Attachments.Add($File) } catch { # non critical error $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " if ($Logger) { $Logger.AddErrorRecord("Error attaching file $Attach`: $ErrorMessage") } else { Write-Error "Error attaching file $Attach`: $ErrorMessage" } } } } } # Sending the Email try { $MailSentTo = "$($MailMessage.To) $($MailMessage.CC) $($MailMessage.BCC)".Trim() if ($pscmdlet.ShouldProcess("$MailSentTo", "Send-Email")) { $SmtpClient.Send($MailMessage) #$att.Dispose(); $MailMessage.Dispose(); return [PSCustomObject] @{ Status = $True Error = "" SentTo = $MailSentTo } } } catch { $MailMessage.Dispose(); return [PSCustomObject] @{ Status = $False Error = $($_.Exception.Message) SentTo = "" } } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. .DESCRIPTION Author: przemyslaw.klys at evotec.pl Project website: https://evotec.xyz/hub/scripts/write-color-ps1/ Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE # Added in 0.5 Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow wc -t "my text" -c yellow -b green wc -text "my text" -c red .NOTES Additional Notes: - TimeFormat https://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx #> [alias('Write-Colour')] [CmdletBinding()] param ([alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine) $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } if ($Text.Count -and $LogFile) { $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop } } catch { $PSCmdlet.WriteError($_) } } } function Get-MimeType { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $FileName ) $MimeMappings = @{ '.jpeg' = 'image/jpeg' '.jpg' = 'image/jpeg' '.png' = 'image/png' } $Extension = [System.IO.Path]::GetExtension( $FileName ) $ContentType = $MimeMappings[ $Extension ] if ([string]::IsNullOrEmpty($ContentType)) { return New-Object System.Net.Mime.ContentType } else { return New-Object System.Net.Mime.ContentType($ContentType) } } function Request-Credentials { [CmdletBinding()] param([string] $UserName, [string] $Password, [switch] $AsSecure, [switch] $FromFile, [switch] $Output, [switch] $NetworkCredentials, [string] $Service) if ($FromFile) { if (($Password -ne '') -and (Test-Path $Password)) { Write-Verbose "Request-Credentials - Reading password from file $Password" $Password = Get-Content -Path $Password } else { if ($Output) { return @{Status = $false; Output = $Service; Extended = 'File with password unreadable.' } } else { Write-Warning "Request-Credentials - Secure password from file couldn't be read. File not readable. Terminating." return } } } if ($AsSecure) { try { $NewPassword = $Password | ConvertTo-SecureString -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " if ($ErrorMessage -like '*Key not valid for use in specified state*') { if ($Output) { return @{Status = $false; Output = $Service; Extended = "Couldn't use credentials provided. Most likely using credentials from other user/session/computer." } } else { Write-Warning -Message "Request-Credentials - Couldn't use credentials provided. Most likely using credentials from other user/session/computer." return } } else { if ($Output) { return @{Status = $false; Output = $Service; Extended = $ErrorMessage } } else { Write-Warning -Message "Request-Credentials - $ErrorMessage" return } } } } else { $NewPassword = $Password } if ($UserName -and $NewPassword) { if ($AsSecure) { $Credentials = New-Object System.Management.Automation.PSCredential($Username, $NewPassword) } else { Try { $SecurePassword = $Password | ConvertTo-SecureString -AsPlainText -Force -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " if ($ErrorMessage -like '*Key not valid for use in specified state*') { if ($Output) { return @{Status = $false; Output = $Service; Extended = "Couldn't use credentials provided. Most likely using credentials from other user/session/computer." } } else { Write-Warning -Message "Request-Credentials - Couldn't use credentials provided. Most likely using credentials from other user/session/computer." return } } else { if ($Output) { return @{Status = $false; Output = $Service; Extended = $ErrorMessage } } else { Write-Warning -Message "Request-Credentials - $ErrorMessage" return } } } $Credentials = New-Object System.Management.Automation.PSCredential($Username, $SecurePassword) } } else { if ($Output) { return @{Status = $false; Output = $Service; Extended = 'Username or/and Password is empty' } } else { Write-Warning -Message 'Request-Credentials - UserName or Password are empty.' return } } if ($NetworkCredentials) { return $Credentials.GetNetworkCredential() } else { return $Credentials } } function Find-LimitedScope { [CmdletBinding()] param( [System.Collections.IDictionary] $ConfigurationParameters, [System.Collections.IDictionary] $CachedUsers ) $Forest = Get-ADForest $UsersInGroups = if ($ConfigurationParameters.RemindersSendToManager.LimitScope) { foreach ($Group in $ConfigurationParameters.RemindersSendToManager.LimitScope.Groups) { foreach ($Domain in $Forest.Domains) { $Server = Get-ADDomainController -Discover -DomainName $Domain try { #$GroupMembers = Get-ADGroupMember -Identity $Group -Server $($Server.HostName) -ErrorAction Stop -Recursive $GroupMembers = Get-ADGroup -Identity $Group -Server $($Server.HostName) -ErrorAction Stop -Properties Members #foreach ($_ in $GroupMembers) { # $CachedUsers["$($_.distinguishedName)"] #} foreach ($_ in $GroupMembers.Members) { $CachedUsers["$($_)"] } } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color @WriteParameters '[e] Managers Limited Scope Error: ', $ErrorMessage -Color White, Red continue } } } } $UsersInGroups } function Get-HTML { [CmdletBinding()] param ( [string] $text ) $text = $text.Split("`r") foreach ($t in $text) { Write-Host $t } } function Get-LowestHighestDays { [cmdletBinding()] param( [System.Collections.IDictionary] $RemindersToUsers ) $HighestLowest = Get-LowestHighestInternal -Rule $RemindersToUsers foreach ($Rule in $RemindersToUsers.Rules) { $PotentialHighestLowest = Get-LowestHighestInternal -Rule $Rule if ($null -eq $PotentialHighestLowest.DayHighest) { continue } if ($null -eq $HighestLowest.DayHighest) { $HighestLowest = $PotentialHighestLowest } else { if ($PotentialHighestLowest.DayHighest -gt $HighestLowest.DayHighest) { $HighestLowest.DayHighest = $PotentialHighestLowest.DayHighest } if ($PotentialHighestLowest.DayLowest -lt $HighestLowest.DayLowest) { $HighestLowest.DayLowest = $PotentialHighestLowest.DayLowest } } } return $HighestLowest } function Get-LowestHighestInternal { [cmdletBinding()] param( [System.Collections.IDictionary] $Rule ) if ($Rule.Enable) { if ($Rule.Reminders -is [System.Collections.IDictionary]) { $DayHighest = Get-HashMaxValue -hashTable $Rule.Reminders $DayLowest = Get-HashMaxValue -hashTable $Rule.Reminders -Lowest } else { [Array] $OrderedDays = $Rule.Reminders | Sort-Object -Unique if ($OrderedDays.Count -gt 0) { $DayHighest = $OrderedDays[-1] $DayLowest = $OrderedDays[0] } } } [ordered] @{ DayHighest = $DayHighest DayLowest = $DayLowest } } function Invoke-ReminderToUsers { [cmdletBinding()] param( [System.Collections.IDictionary] $RemindersToUsers, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $FormattingParameters, [System.Collections.IDictionary] $ConfigurationParameters, [Array] $Users ) Invoke-ReminderToUsersInternal -Rule $RemindersToUsers -EmailParameters $EmailParameters -ConfigurationParameters $ConfigurationParameters -FormattingParameters $FormattingParameters -Users $Users if (-not $Limits.TestingLimitReached) { foreach ($Rule in $RemindersToUsers.Rules) { Invoke-ReminderToUsersInternal -Rule $Rule -EmailParameters $EmailParameters -ConfigurationParameters $ConfigurationParameters -FormattingParameters $FormattingParameters -Users $Users if ($Limits.TestingLimitReached) { break } } } } function Invoke-ReminderToUsersInternal { [cmdletBinding()] param( [System.Collections.IDictionary] $Rule, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $FormattingParameters, [System.Collections.IDictionary] $ConfigurationParameters, [Array] $Users ) $Limits = @{ TestingLimitReached = $false } if ($Rule.Enable -eq $true) { #$Today = Get-Date $EmailBody = Set-EmailHead -FormattingOptions $FormattingParameters $Image = Set-EmailReportBranding -FormattingOptions $FormattingParameters if ($Rule.Template) { $EmailBody += Set-EmailFormatting -Template $Rule.Template -FormattingParameters $FormattingParameters -ConfigurationParameters $ConfigurationParameters -Image $Image } else { $EmailBody += Set-EmailFormatting -Template $FormattingParameters.Template -FormattingParameters $FormattingParameters -ConfigurationParameters $ConfigurationParameters -Image $Image } Write-Color @WriteParameters '[i] Starting processing ', 'Users', ' section' -Color White, Yellow, White if ($Rule.Reminders -is [System.Collections.IDictionary]) { [Array] $DaysToExpire = ($Rule.Reminders).Values | Sort-Object -Unique } else { [Array] $DaysToExpire = $Rule.Reminders | Sort-Object -Unique } $Count = 0 foreach ($u in $Users) { if ($Limits.TestingLimitReached -eq $true) { break } if ($null -eq $Rule.PasswordNeverExpires -or $null -eq $Rule.PasswordNeverExpiresDays -or $Rule.PasswordNeverExpires -eq $false) { # This is standard situation that user expires normally if ($u.PasswordNeverExpires -eq $true -or $u.PasswordAtNextLogon -eq $true) { continue } } elseif ($Rule.PasswordNeverExpires -eq $true -and $Rule.PasswordNeverExpiresDays -is [int]) { # this is for situation where we want to monitor PasswordNeverExpires if ($u.PasswordAtNextLogon -eq $true) { continue } if ($u.PasswordNeverExpires -eq $false) { continue } } else { Write-Color @WriteParameters '[i] Something went wrong as there are no rules matching...' -Color Red continue } if ($u.PasswordNeverExpires -eq $true) { # If we're here it means we want to get only users that never expire $PretendedDateExpiration = ($u.PasswordLastSet).AddDays($Rule.PasswordNeverExpiresDays) $PretendedDaysToExpire = (New-TimeSpan -Start (Get-Date) -End ($PretendedDateExpiration)).Days # we overwrite dates $u.DateExpiry = $PretendedDateExpiration $u.DaysToExpire = $PretendedDaysToExpire } # This makes sure to apply notifications if ($Rule.LimitGroup) { $Found = $false foreach ($LimitGroup in $Rule.LimitGroup) { if ($LimitGroup -in $u.MemberOf) { $Found = $true break } } if (-not $Found) { continue } } if ($Rule.LimitOU) { $Found = $false foreach ($LimitOU in $Rule.LimitOU) { if ($u.OrganizationalUnit -like $LimitOU) { $Found = $true break } } if (-not $Found) { continue } } $Script:UsersApplicable.Add($u) # this is standard way - check if user is expiring within the correct date if ($u.DaysToExpire -notin $DaysToExpire) { continue } if ($u.EmailAddress -like '*@*') { $Count++ Write-Color @WriteParameters -Text "[i] User ", "$($u.DisplayName)", " expires in ", "$($u.DaysToExpire)", " days (", "$($u.DateExpiry)", ")." -Color White, Yellow, White, Red, White, Red, White $TemporaryBody = Set-EmailReplacements -Replacement $EmailBody -User $u -FormattingParameters $FormattingParameters -EmailParameters $EmailParameters -Day $u.DaysToExpire $EmailSubject = Set-EmailReplacements -Replacement $EmailParameters.EmailSubject -User $u -FormattingParameters $FormattingParameters -EmailParameters $EmailParameters -Day $u.DaysToExpire #$u.DaysToExpire = $Day.Value if ($Rule.RemindersDisplayOnly -eq $true) { Write-Color @WriteParameters -Text "[i] Pretending to send email to ", "$($u.EmailAddress)", " ...", "Success" -Color White, Green, White, Green $EmailSent = [ordered] @{ } $EmailSent.Status = $false $EmailSent.SentTo = 'N/A' } else { $EmailSplat = @{ EmailParameters = $EmailParameters Body = $TemporaryBody Subject = $EmailSubject } if ($FormattingParameters.CompanyBranding.Inline) { $EmailSplat.InlineAttachments = @{ logo = $FormattingParameters.CompanyBranding.Logo } } if ($Rule.SendToDefaultEmail -eq $false) { Write-Color @WriteParameters -Text "[i] Sending email to ", "$($u.EmailAddress)", " ..." -Color White, Green -NoNewLine $EmailSplat.To = $u.EmailAddress } else { Write-Color @WriteParameters -Text "[i] Sending email to users is disabled. Sending email to default value: ", "$($EmailParameters.EmailTo) ", "..." -Color White, Yellow, White -NoNewLine } $EmailSent = Send-Email @EmailSplat if ($EmailSent.Status -eq $true) { Write-Color -Text "Done" -Color "Green" -LogFile $WriteParameters.LogFile } else { Write-Color -Text "Failed!" -Color "Red" -LogFile $WriteParameters.LogFile } } Add-Member -InputObject $u -NotePropertyName "EmailSent" -NotePropertyValue $EmailSent.Status Add-Member -InputObject $u -NotePropertyName "EmailSentTo" -NotePropertyValue $EmailSent.SentTo } else { Add-Member -InputObject $u -NotePropertyName "EmailSent" -NotePropertyValue $false Add-Member -InputObject $u -NotePropertyName "EmailSentTo" -NotePropertyValue 'Not available' Write-Color @WriteParameters -Text "[i] User ", "$($u.DisplayName)", " expires in ", "$($u.DaysToExpire)", " days (", "$($u.DateExpiry)", "). However user has no email address and will be skipped." -Color White, Yellow, White, Red, White, Red, White } $u if ($Rule.SendCountMaximum -eq $Count) { Write-Color @WriteParameters -Text "[i] Sending email to maximum number of users ", "$($Rule.SendCountMaximum) ", "has been reached. Skipping..." -Color White, Yellow, White $Limits.TestingLimitReached = $true break } } Write-Color @WriteParameters '[i] Ending processing ', 'Users', ' section' -Color White, Yellow, White } else { Write-Color @WriteParameters '[i] Skipping processing ', 'Users', ' section' -Color White, Yellow, White } } $script:WriteParameters = @{ ShowTime = $true LogFile = "" TimeFormat = "yyyy-MM-dd HH:mm:ss" } function Set-EmailBody { [CmdletBinding()] param( [Object] $TableData, [alias('TableWelcomeMessage')][string] $TableMessageWelcome, [string] $TableMessageNoData = 'No changes happened during that period.' ) $Body = "<p><i><u>$TableMessageWelcome</u></i></p>" if ($($TableData | Measure-Object).Count -gt 0) { $Body += $TableData | ConvertTo-Html -Fragment | Out-String # $Body += "</p>" } else { $Body += "<p><i>$TableMessageNoData</i></p>" } return $body } function Set-EmailBodyReplacementTable { [CmdletBinding()] [alias('Set-EmailBodyTableReplacement')] param ( [string] $Body, [string] $TableName, [Array] $TableData ) $TableData = $TableData | ConvertTo-Html -Fragment | Out-String $Body = $Body -replace "<<$TableName>>", $TableData return $Body } function Set-EmailFormatting { [CmdletBinding()] param ( $Template, [System.Collections.IDictionary] $FormattingParameters, [System.Collections.IDictionary] $ConfigurationParameters, [PSCustomObject] $Logger, [switch] $SkipNewLines, [string[]] $AddAfterOpening, [string[]] $AddBeforeClosing, [string] $Image ) if ($ConfigurationParameters) { $WriteParameters = $ConfigurationParameters.DisplayConsole } else { $WriteParameters = @{ ShowTime = $true; LogFile = ""; TimeFormat = "yyyy-MM-dd HH:mm:ss" } } if ($Image) { $Template = $Template -replace '<<Image>>', $Image } $Body = "<body>" if ($AddAfterOpening) { $Body += $AddAfterOpening } if (-not $SkipNewLines) { $Template = $Template.Split("`n") # https://blogs.msdn.microsoft.com/timid/2014/07/09/one-liner-fun-with-multi-line-blocktext-and-split-split/ if ($Logger) { $Logger.AddInfoRecord("Preparing template - adding HTML <BR> tags...") } else { Write-Color @WriteParameters -Text "[i] Preparing template ", "adding", " HTML ", "<BR>", " tags." -Color White, Yellow, White, Yellow } foreach ($t in $Template) { $Body += "$t<br>" } } else { $Body += $Template } foreach ($style in $FormattingParameters.Styles.GetEnumerator()) { foreach ($value in $style.Value) { if ($value -eq "") { continue } if ($Logger) { $Logger.AddInfoRecord("Preparing template - adding HTML $($style.Name) tag for $value.") } else { Write-Color @WriteParameters -Text "[i] Preparing template ", "adding", " HTML ", "$($style.Name)", " tag for ", "$value", ' tags...' -Color White, Yellow, White, Yellow, White, Yellow } $Body = $Body.Replace($value, "<$($style.Name)>$value</$($style.Name)>") } } foreach ($color in $FormattingParameters.Colors.GetEnumerator()) { foreach ($value in $color.Value) { if ($value -eq "") { continue } if ($Logger) { $Logger.AddInfoRecord("Preparing template - adding HTML $($color.Name) tag for $value.") } else { Write-Color @WriteParameters -Text "[i] Preparing template ", "adding", " HTML ", "$($color.Name)", " tag for ", "$value", ' tags...' -Color White, Yellow, White, Yellow, White, Yellow } $Body = $Body.Replace($value, "<span style=color:$($color.Name)>$value</span>") } } foreach ($links in $FormattingParameters.Links.GetEnumerator()) { foreach ($link in $links.Value) { if ($link.Link -like "*@*") { if ($Logger) { $Logger.AddInfoRecord("Preparing template - adding EMAIL Links for $($links.Key).") } else { Write-Color @WriteParameters -Text "[i] Preparing template ", "adding", " EMAIL ", "Links for", " $($links.Key)..." -Color White, Yellow, White, White, Yellow, White } $Body = $Body -replace "<<$($links.Key)>>", "<span style=color:$($link.Color)><a href='mailto:$($link.Link)?subject=$($Link.Subject)'>$($Link.Text)</a></span>" } else { if ($Logger) { $Logger.AddInfoRecord("[i] Preparing template - adding HTML Links for $($links.Key)") } else { Write-Color @WriteParameters -Text "[i] Preparing template ", "adding", " HTML ", "Links for", " $($links.Key)..." -Color White, Yellow, White, White, Yellow, White } $Body = $Body -replace "<<$($links.Key)>>", "<span style=color:$($link.Color)><a href='$($link.Link)'>$($Link.Text)</a></span>" } } } if ($AddAfterOpening) { $Body += $AddBeforeClosing } $Body += '</body>' if ($ConfigurationParameters) { if ($ConfigurationParameters.DisplayTemplateHTML -eq $true) { Get-HTML($Body) } } return $Body } function Set-EmailHead { [cmdletBinding()] param( [System.Collections.IDictionary] $FormattingOptions ) $head = @" <!DOCTYPE html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta content="width=device-width, initial-scale=1" name="viewport"> </head> <style> BODY { background-color: white; font-family: $($FormattingOptions.FontFamily); font-size: $($FormattingOptions.FontSize); } TABLE { border-width: 1px; border-style: solid; border-color: black; border-collapse: collapse; font-family: $($FormattingOptions.FontTableDataFamily); font-size: $($FormattingOptions.FontTableDataSize); } TH { border-width: 1px; padding: 3px; border-style: solid; border-color: black; background-color: #00297A; color: white; font-family: $($FormattingOptions.FontTableHeadingFamily); font-size: $($FormattingOptions.FontTableHeadingSize); } TR { font-family: $($FormattingOptions.FontTableDataFamily); font-size: $($FormattingOptions.FontTableDataSize); } UL { font-family: $($FormattingOptions.FontFamily); font-size: $($FormattingOptions.FontSize); } LI { font-family: $($FormattingOptions.FontFamily); font-size: $($FormattingOptions.FontSize); } TD { border-width: 1px; padding-right: 2px; padding-left: 2px; padding-top: 0px; padding-bottom: 0px; border-style: solid; border-color: black; background-color: white; font-family: $($FormattingOptions.FontTableDataFamily); font-size: $($FormattingOptions.FontTableDataSize); } H2 { font-family: $($FormattingOptions.FontHeadingFamily); font-size: $($FormattingOptions.FontHeadingSize); } P { font-family: $($FormattingOptions.FontFamily); font-size: $($FormattingOptions.FontSize); } </style> </head> "@ return $Head } function Set-EmailReplacements { [CmdletBinding()] param( [string] $Replacement, [PSCustomObject] $User, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $FormattingParameters, [int] $Day ) $Replacement = $Replacement -replace "<<DisplayName>>", $user.DisplayName $Replacement = $Replacement -replace "<<DateExpiry>>", $user.DateExpiry $Replacement = $Replacement -replace "<<GivenName>>", $user.GivenName $Replacement = $Replacement -replace "<<Surname>>", $user.Surname $Replacement = $Replacement -replace "<<TimeToExpire>>", $Day $Replacement = $Replacement -replace "<<ManagerDisplayName>>", $user.Manager $Replacement = $Replacement -replace "<<ManagerEmail>>", $user.ManagerEmail if ($FormattingParameters.Conditions) { foreach ($Key in $FormattingParameters.Conditions.Keys) { $Found = $false $ReplaceFrom = "<<$Key>>" $DefaultReplaceTo = $FormattingParameters.Conditions["$Key"]['DefaultCondition'] foreach ($Condition in $FormattingParameters.Conditions["$Key"].Keys | Where-Object { $_ -ne 'DefaultCondition' }) { if ($FormattingParameters.Conditions["$Key"]["$Condition"]) { foreach ($SubKey in $FormattingParameters.Conditions["$Key"]["$Condition"].Keys) { if ($SubKey -eq $User.$Condition) { $Replacement = $Replacement -replace "$ReplaceFrom", $FormattingParameters.Conditions["$Key"]["$Condition"][$SubKey] $Found = $true } } } } if (-not $Found) { $Replacement = $Replacement -replace "$ReplaceFrom", $DefaultReplaceTo } } } return $Replacement } function Set-EmailReportBranding { [cmdletBinding()] param( [alias('FormattingOptions')] $FormattingParameters ) if ($FormattingParameters.CompanyBranding.Link) { $Report = "<a style=`"text-decoration:none`" href=`"$($FormattingParameters.CompanyBranding.Link)`" class=`"clink logo-container`">" } else { $Report = '' } if ($FormattingParameters.CompanyBranding.Inline) { $Report += "<img width=<fix> height=<fix> src=`"cid:logo`" border=`"0`" class=`"company-logo`" alt=`"company-logo`"></a>" } else { $Report += "<img width=<fix> height=<fix> src=`"$($FormattingParameters.CompanyBranding.Logo)`" border=`"0`" class=`"company-logo`" alt=`"company-logo`"></a>" } if ($FormattingParameters.CompanyBranding.Width -ne "") { $Report = $Report -replace "width=<fix>", "width=$($FormattingParameters.CompanyBranding.Width)" } else { $Report = $Report -replace "width=<fix>", "" } if ($FormattingParameters.CompanyBranding.Height -ne "") { $Report = $Report -replace "height=<fix>", "height=$($FormattingParameters.CompanyBranding.Height)" } else { $Report = $Report -replace "height=<fix>", "" } return $Report } function Set-EmailReportDetails { [CmdletBinding()] param( [System.Collections.IDictionary] $FormattingOptions, [System.Collections.IDictionary] $ReportOptions, [timespan] $TimeToGenerate, [int] $CountUsersImminent, [int] $CountUsersCountdownStarted, [int] $CountUsersAlreadyExpired, [int] $CountUsersNotified ) $DateReport = Get-Date # HTML Report settings $Report = @( @" <p> <strong>Report Time:</strong> $DateReport <br> <strong>Time to generate:</strong> $($TimeToGenerate.Hours) hours, $($TimeToGenerate.Minutes) minutes, $($TimeToGenerate.Seconds) seconds, $($TimeToGenerate.Milliseconds) milliseconds <br> <strong>Account Executing Report :</strong> $env:userdomain\$($env:username.toupper()) on $($env:ComputerName.toUpper()) <br> <strong>Users notified: </strong> $CountUsersNotified <br> <strong>Users expiring countdown started: </strong> $CountUsersCountdownStarted <br> <strong>Users expiring soon: </strong> $CountUsersImminent <br> <strong>Users already expired count: </strong> $CountUsersAlreadyExpired <br> </p> "@ foreach ($ip in $ReportOptions.MonitoredIps.Values) { "<li>ip:</strong> $ip</li>" } '</ul>' '</p>' ) return $Report } function Test-Prerequisits { [CmdletBinding()] param() try { $null = Get-ADForest } catch { if ($_.Exception -match "Unable to find a default server with Active Directory Web Services running.") { Write-Color @script:WriteParameters "[-] ", "Active Directory", " not found. Please run this script with access to ", "Domain Controllers." -Color White, Red, White, Red } Write-Color @script:WriteParameters "[-] ", "Error: $($_.Exception.Message)" -Color White, Red Exit } } function Find-PasswordExpiryCheck { [CmdletBinding()] param( [string] $AdditionalProperties, [Array] $ConditionProperties, [System.Collections.IDictionary] $WriteParameters, [System.Collections.IDictionary] $CachedUsers, [System.Collections.IDictionary] $CachedUsersPrepared, [System.Collections.IDictionary] $CachedManagers ) if ($null -eq $WriteParameters) { $WriteParameters = @{ ShowTime = $true LogFile = "" TimeFormat = "yyyy-MM-dd HH:mm:ss" } } $Properties = @( 'Manager', 'DisplayName', 'GivenName', 'Surname', 'SamAccountName', 'EmailAddress', 'msDS-UserPasswordExpiryTimeComputed', 'PasswordExpired', 'PasswordLastSet', 'PasswordNotRequired', 'Enabled', 'PasswordNeverExpires', 'Mail', 'MemberOf' if ($AdditionalProperties) { $AdditionalProperties } if ($ConditionProperties) { $ConditionProperties } ) # We're caching all users to make sure it's speedy gonzales when querying for Managers if (-not $CachedUsers) { $CachedUsers = [ordered] @{ } } if (-not $CachedUsersPrepared) { $CachedUsersPrepared = [ordered] @{ } } if (-not $CachedManagers) { $CachedManagers = [ordered] @{} } Write-Color @WriteParameters -Text "[i] Discovering forest information" -Color White, Yellow, White, Yellow, White, Yellow, White $Forest = Get-ADForest $Users = @( foreach ($Domain in $Forest.Domains) { try { Write-Color @WriteParameters -Text "[i] Discovering DC for domain ", "$($Domain)", " in forest ", $Forest.Name -Color White, Yellow, White, Yellow, White, Yellow, White $Server = Get-ADDomainController -Discover -DomainName $Domain -ErrorAction Stop #$Users = Get-ADUser -Server $Server -Filter { Enabled -eq $True -and PasswordNeverExpires -eq $False -and PasswordLastSet -gt 0 -and PasswordNotRequired -ne $True } -Properties $Properties -ErrorAction Stop Write-Color @WriteParameters -Text "[i] Getting users from ", "$($Domain)", " using ", $Server.Hostname -Color White, Yellow, White, Yellow, White, Yellow, White # We query all users instead of using filter. Since we need manager field and manager data this way it should be faster (query once - get it all) $DomainUsers = Get-ADUser -Server $($Server.HostName) -Filter '*' -Properties $Properties -ErrorAction Stop foreach ($_ in $DomainUsers) { Add-Member -InputObject $_ -Value $Domain -Name 'Domain' -Force -Type NoteProperty $CachedUsers["$($_.DistinguishedName)"] = $_ # We reuse filtering, account is enabled, password is required and password is not set to change on next logon if ($_.Enabled -eq $true -and $_.PasswordNotRequired -ne $true -and $null -ne $_.PasswordLastSet) { #if ($_.Enabled -eq $true -and $_.PasswordNeverExpires -eq $false -and $null -ne $_.PasswordLastSet -and $_.PasswordNotRequired -ne $true) { $_ } } } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color @WriteParameters '[e] Error: ', $ErrorMessage -Color White, Red } } ) Write-Color @WriteParameters -Text "[i] Preparing all users for password expirations in forest ", $Forest.Name -Color White, Yellow, White, Yellow, White, Yellow, White $ProcessedUsers = foreach ($_ in $Users) { $UserManager = $CachedUsers["$($_.Manager)"] if ($AdditionalProperties) { # fix this for a user $EmailTemp = $_.$AdditionalProperties if ($EmailTemp -like '*@*') { $EmailAddress = $EmailTemp } else { $EmailAddress = $_.EmailAddress } # Fix this for manager as well if ($UserManager) { if ($UserManager.$AdditionalProperties -like '*@*') { $UserManager.Mail = $UserManager.$AdditionalProperties } } } else { $EmailAddress = $_.EmailAddress } if ($_."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) { # This is standard situation where users password is expiring as needed try { $DateExpiry = ([datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")) } catch { $DateExpiry = $_."msDS-UserPasswordExpiryTimeComputed" } try { $DaysToExpire = (New-TimeSpan -Start (Get-Date) -End ([datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed"))).Days } catch { $DaysToExpire = $null } $PasswordNeverExpires = $_.PasswordNeverExpires } else { # This is non-standard situation. This basically means most likely Fine Grained Group Policy is in action where it makes PasswordNeverExpires $true # Since FGP policies are a bit special they do not tick the PasswordNeverExpires box, but at the same time value for "msDS-UserPasswordExpiryTimeComputed" is set to 9223372036854775807 $PasswordNeverExpires = $true } if ($PasswordNeverExpires -or $null -eq $_.PasswordLastSet) { $DateExpiry = $null $DaysToExpire = $null } $MyUser = [ordered] @{ UserPrincipalName = $_.UserPrincipalName Domain = $_.Domain SamAccountName = $_.SamAccountName DisplayName = $_.DisplayName GivenName = $_.GivenName Surname = $_.Surname EmailAddress = $EmailAddress PasswordExpired = $_.PasswordExpired PasswordLastSet = $_.PasswordLastSet PasswordNotRequired = $_.PasswordNotRequired PasswordNeverExpires = $PasswordNeverExpires #PasswordAtNextLogon = $null -eq $_.PasswordLastSet Manager = $UserManager.Name ManagerEmail = $UserManager.Mail ManagerDN = $_.Manager DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire OrganizationalUnit = ConvertFrom-DistinguishedName -DistinguishedName $_.DistinguishedName -ToOrganizationalUnit MemberOf = $_.MemberOf } foreach ($Property in $ConditionProperties) { $MyUser["$Property"] = $_.$Property } [PSCustomObject] $MyUser $CachedUsersPrepared["$($_.DistinguishedName)"] = $MyUser } foreach ($_ in $CachedUsersPrepared.Keys) { $ManagerDN = $CachedUsersPrepared[$_]['ManagerDN'] if ($ManagerDN) { $Manager = $CachedUsers[$ManagerDN] $MyUser = [ordered] @{ UserPrincipalName = $Manager.UserPrincipalName Domain = $Manager.Domain SamAccountName = $Manager.SamAccountName DisplayName = $Manager.DisplayName GivenName = $Manager.GivenName Surname = $Manager.Surname DistinguishedName = $ManagerDN } foreach ($Property in $ConditionProperties) { $MyUser["$Property"] = $_.$Property } $CachedManagers[$ManagerDN] = $MyUser } } $ProcessedUsers } #$Test = Find-PasswordExpiryCheck -AdditionalProperties 'extensionAttribute13' #$Test | Format-Table -AutoSize * Function Start-PasswordExpiryCheck { [CmdletBinding()] param ( [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $FormattingParameters, [System.Collections.IDictionary] $ConfigurationParameters ) $time = [System.Diagnostics.Stopwatch]::StartNew() # Timer Start $WriteParameters = $ConfigurationParameters.DisplayConsole if ($WriteParameters.LogFile) { $Folder = $WriteParameters.LogFile | Split-Path if (-not (Test-Path -Path $Folder)) { $null = New-Item -ItemType Directory -Path $Folder -Force if (-not (Test-Path -Path $Folder)) { Write-Color "[e] Can't created $Folder for logging. Terminating..." -Color Red return } } } Test-Prerequisits # Overwritting whatever user set as this is what it should be, always for proper display if ($EmailParameters.EmailEncoding -or $EmailParameters.EmailSubjectEncoding -or $EmailParameters.EmailBodyEncoding) { Write-Color @WriteParameters '[e] Setting encoding was depracated. Its set automatically now to utf8' -Color Red } $EmailParameters.EmailEncoding = "" $EmailParameters.EmailSubjectEncoding = "" $EmailParameters.EmailBodyEncoding = "" # This takes care of additional fields for all rules (native and additional) $FieldName = @( $ConfigurationParameters.RemindersSendToUsers.UseAdditionalField foreach ($Rule in $ConfigurationParameters.RemindersSendToUsers.Rules | Where-Object { $_.Enable -eq $true }) { $Rule.UseAdditionalField } ) | Sort-Object -Unique $Today = Get-Date $CachedUsers = [ordered] @{ } $CachedUsersPrepared = [ordered] @{ } $CachedManagers = [ordered] @{ } [Array] $ConditionProperties = if ($FormattingParameters.Conditions) { foreach ($Key in $FormattingParameters.Conditions.Keys) { foreach ($Condition in $FormattingParameters.Conditions["$Key"].Keys | Where-Object { $_ -ne 'DefaultCondition' }) { $Condition } } } [Array] $Users = Find-PasswordExpiryCheck -AdditionalProperties $FieldName -ConditionProperties $ConditionProperties -WriteParameters $WriteParameters -CachedUsers $CachedUsers -CachedUsersPrepared $CachedUsersPrepared -CachedManagers $CachedManagers | Sort-Object DateExpiry # This will make sure to catch only applicable users. Since there are multiple rules possible we can't use $Users as our source of truth $Script:UsersApplicable = [System.Collections.Generic.List[PSCustomObject]]::new() #region Send Emails to Users [Array] $UsersNotified = Invoke-ReminderToUsers -RemindersToUsers $ConfigurationParameters.RemindersSendToUsers -EmailParameters $EmailParameters -ConfigurationParameters $ConfigurationParameters -FormattingParameters $FormattingParameters -Users $Users # Build a report for expired users [Array] $UsersExpired = $Script:UsersApplicable | Where-Object { $null -ne $_.DateExpiry -and $_.DateExpiry -lt $Today } #region Send Emails to Managers [Array] $ManagersReceived = if ($ConfigurationParameters.RemindersSendToManager.Enable -eq $true) { Write-Color @WriteParameters '[i] Starting processing ', 'Managers', ' section' -Color White, Yellow, White # preparing email $EmailSubject = $ConfigurationParameters.RemindersSendToManager.ManagersEmailSubject $EmailBody = Set-EmailHead -FormattingOptions $FormattingParameters $EmailReportBranding = Set-EmailReportBranding -FormattingOptions $FormattingParameters $EmailBody += Set-EmailFormatting -Template $FormattingParameters.TemplateForManagers ` -FormattingParameters $FormattingParameters ` -ConfigurationParameters $ConfigurationParameters ` -Image $EmailReportBranding # preparing manager lists if ($ConfigurationParameters.RemindersSendToManager.LimitScope.Groups) { # send emails to managers only if those people are in limited scope groups [Array] $LimitedScopeMembers = Find-LimitedScope -ConfigurationParameters $ConfigurationParameters -CachedUsers $CachedUsersPrepared [Array] $UsersWithManagers = foreach ($_ in $UsersNotified) { if ($LimitedScopeMembers.UserPrincipalName -contains $_.UserPrincipalName) { if ($null -ne $_.ManagerEmail) { $_ } } } } else { [Array] $UsersWithManagers = foreach ($_ in $UsersNotified) { if ($null -ne $_.ManagerEmail) { $_ } } # $UsersWithManagers = $UsersNotified | Where-Object { $null -ne $_.ManagerEmail } } # Find managers with emails. Make sure only unique is added to the list $ManagersEmails = [System.Collections.Generic.List[string]]::new() foreach ($u in $UsersWithManagers) { if ($ManagersEmails -notcontains $u.ManagerEmail) { $ManagersEmails.Add($u.ManagerEmail) } } Write-Color @WriteParameters '[i] Preparing package for managers with emails ', "$($UsersWithManagers.Count) ", 'users to process with', ' manager filled in', ' where unique managers ', "$($ManagersEmails.Count)" -Color White, Yellow, White, Yellow, White, Yellow # processing one manager at time $Count = 0 foreach ($m in $ManagersEmails) { $Count++ # preparing users belonging to manager $ColumnNames = 'UserPrincipalName', 'DisplayName', 'DateExpiry', 'PasswordExpired', 'SamAccountName', 'Manager', 'ManagerEmail', 'PasswordLastSet' [Array] $UsersNotifiedManagers = $UsersWithManagers | Where-Object { $_.ManagerEmail -eq $m } [string] $ManagerDN = $UsersNotifiedManagers[0].ManagerDN $ManagerFull = $CachedManagers[$ManagerDN] if ($ConfigurationParameters.RemindersSendToManager.Reports.IncludePasswordNotificationsSent.IncludeNames -ne '') { $UsersNotifiedManagers = $UsersNotifiedManagers | Select-Object $ConfigurationParameters.RemindersSendToManager.Reports.IncludePasswordNotificationsSent.IncludeNames } else { $UsersNotifiedManagers = $UsersNotifiedManagers | Select-Object 'UserPrincipalName', 'DisplayName', 'DateExpiry', 'DaysToExpire', 'SamAccountName', 'Manager', 'ManagerEmail', 'PasswordLastSet', 'EmailSent', 'EmailSentTo' } if ($ConfigurationParameters.RemindersSendToManager.Reports.IncludePasswordNotificationsSent.Enabled -eq $true) { foreach ($u in $UsersNotifiedManagers) { Write-Color @WriteParameters -Text '[-] User ', "$($u.DisplayName) ", " Managers Email (", "$($m)", ')' -Color White, Yellow, White, Yellow, White } } if ($ConfigurationParameters.RemindersSendToManager.RemindersDisplayOnly -eq $true) { Write-Color @WriteParameters -Text "[i] Pretending to send email to manager email ", "$($m)", " ...", "Success" -Color White, Green, White, Green $EmailSent = @{ } $EmailSent.Status = $false $EmailSent.SentTo = 'N/A' } else { $TemporaryBody = $EmailBody $TemporaryBody = Set-EmailBodyReplacementTable -Body $TemporaryBody -TableName 'ManagerUsersTable' -TableData $UsersNotifiedManagers $TemporaryBody = Set-EmailReplacements -Replacement $TemporaryBody -User $u -FormattingParameters $FormattingParameters -EmailParameters $EmailParameters #-Day '' if ($ConfigurationParameters.Debug.DisplayTemplateHTML -eq $true) { Get-HTML -text $TemporaryBody } $EmailSplat = @{ EmailParameters = $EmailParameters Body = $TemporaryBody Subject = $EmailSubject } if ($FormattingParameters.CompanyBranding.Inline) { $EmailSplat.InlineAttachments = @{ logo = $FormattingParameters.CompanyBranding.Logo } } if ($ConfigurationParameters.RemindersSendToManager.SendToDefaultEmail -eq $false) { Write-Color @WriteParameters -Text "[i] Sending email to managers email ", "$($m)", " ..." -Color White, Green -NoNewLine $EmailSplat.To = $m } else { Write-Color @WriteParameters -Text "[i] Sending email to managers is disabled. Sending email to default value: ", "$($EmailParameters.EmailTo) ", "..." -Color White, Yellow, White -NoNewLine } $EmailSent = Send-Email @EmailSplat if ($EmailSent.Status -eq $true) { Write-Color -Text "Done" -Color "Green" -LogFile $WriteParameters.LogFile } else { Write-Color -Text "Failed!" -Color "Red" -LogFile $WriteParameters.LogFile } } $ManagerFull['EmailSent'] = $EmailSent.Status $ManagerFull['EmailSentTo'] = $EmailSent.SentTo if ($ConfigurationParameters.RemindersSendToManager.Reports.IncludeManagersPasswordNotificationsSent.IncludeNames.Count -gt 0) { ([PSCustomObject] $ManagerFull) | Select-Object -Property $ConfigurationParameters.RemindersSendToManager.Reports.IncludeManagersPasswordNotificationsSent.IncludeNames } else { ([PSCustomObject] $ManagerFull) | Select-Object -Property 'UserPrincipalName', 'Domain', 'DisplayName', 'SamAccountName', 'EmailSent', 'EmailSentTo' } if ($ConfigurationParameters.RemindersSendToManager.SendCountMaximum -eq $Count) { Write-Color @WriteParameters -Text "[i] Sending email to maximum number of managers ", "$($ConfigurationParameters.RemindersSendToManager.SendCountMaximum) ", " has been reached. Skipping..." -Color White, Yellow, White -NoNewLine break } } Write-Color @WriteParameters '[i] Ending processing ', 'Managers', ' section' -Color White, Yellow, White } else { Write-Color @WriteParameters '[i] Skipping processing ', 'Managers', ' section' -Color White, Yellow, White } #endregion Send Emails to Managers if ($ConfigurationParameters.DisableExpiredUsers.Enable -eq $true) { Write-Color @WriteParameters '[i] Starting processing ', 'Disable Expired Users', ' section' -Color White, Yellow, White foreach ($U in $UsersExpired) { if ($ConfigurationParameters.DisableExpiredUsers.DisplayOnly) { Write-Color @WriteParameters -Text "[i] User ", "$($u.DisplayName)", " expired on (", "$($u.DateExpiry)", "). Pretending to disable acoount..." -Color White, Yellow, White, Red, White, Red, White } else { Write-Color @WriteParameters -Text "[i] User ", "$($u.DisplayName)", " expired on (", "$($u.DateExpiry)", "). Disabling..." -Color White, Yellow, White, Red, White, Red, White Disable-ADAccount -Identity $u.SamAccountName -Confirm:$false } } Write-Color @WriteParameters '[i] Ending processing ', 'Disable Expired Users', ' section' -Color White, Yellow, White } #region Send Emails to Admins if ($ConfigurationParameters.RemindersSendToAdmins.Enable -eq $true) { Write-Color @WriteParameters '[i] Starting processing ', 'Administrators', ' section' -Color White, Yellow, White $SummaryDays = Get-LowestHighestDays -RemindersToUsers $ConfigurationParameters.RemindersSendToUsers $DayHighest = $SummaryDays.DayHighest $DayLowest = $SummaryDays.DayLowest if ($null -eq $DayHighest -or $null -eq $DayLowest) { # Skip reports because reminders are not set at all - weird <# $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeSummary.Enabled = $false $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludePasswordNotificationsSent.Enabled = $false $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeManagersPasswordNotificationsSent.Enabled = $false $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringImminent.Enabled = $false $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringCountdownStarted.Enabled = $false $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpired.Enabled = $false #> } $DateCountdownStart = (Get-Date).AddDays($DayHighest).Date $DateIminnent = (Get-Date).AddDays($DayLowest).Date $Today = Get-Date $ColumnNames = 'UserPrincipalName', 'DisplayName', 'DateExpiry', 'DaysToExpire', 'PasswordExpired', 'SamAccountName', 'Manager', 'ManagerEmail', 'PasswordLastSet', 'PasswordNeverExpires' if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludePasswordNotificationsSent.IncludeNames -gt 0) { $UsersNotified = $UsersNotified | Select-Object $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludePasswordNotificationsSent.IncludeNames } else { $UsersNotified = $UsersNotified | Select-Object $ColumnNames, 'EmailSent', 'EmailSentTo' } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringImminent.IncludeNames.Count -gt 0) { $ExpiringIminent = $Script:UsersApplicable | Where-Object { $null -ne $_.DateExpiry -and ($_.DateExpiry -lt $DateIminnent -and $_.DateExpiry -gt $Today) -and $_.PasswordExpired -eq $false } | Select-Object $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringImminent.IncludeNames } else { $ExpiringIminent = $Script:UsersApplicable | Where-Object { $null -ne $_.DateExpiry -and ($_.DateExpiry -lt $DateIminnent -and $_.DateExpiry -gt $Today) -and $_.PasswordExpired -eq $false } | Select-Object $ColumnNames } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringCountdownStarted.IncludeNames -gt 0) { $ExpiringCountdownStarted = $Script:UsersApplicable | Where-Object { $null -ne $_.DateExpiry -and ($_.DateExpiry -lt $DateCountdownStart -and $_.DateExpiry -gt $DateIminnent) -and $_.PasswordExpired -eq $false } | Select-Object $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringCountdownStarted.IncludeNames } else { $ExpiringCountdownStarted = $Script:UsersApplicable | Where-Object { $null -ne $_.DateExpiry -and ($_.DateExpiry -lt $DateCountdownStart -and $_.DateExpiry -gt $DateIminnent) -and $_.PasswordExpired -eq $false } | Select-Object $ColumnNames } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpired.IncludeNames -gt 0) { $UsersExpired = $UsersExpired | Select-Object $ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpired.IncludeNames } else { $UsersExpired = $UsersExpired | Select-Object $ColumnNames } $EmailBody = Set-EmailHead -FormattingOptions $FormattingParameters $EmailBody += "<body>" $EmailBody += Set-EmailReportBranding -FormattingOptions $FormattingParameters $EmailBody += Set-EmailReportDetails -FormattingOptions $FormattingParameters ` -ReportOptions $ReportOptions ` -TimeToGenerate $Time.Elapsed ` -CountUsersCountdownStarted $($ExpiringCountdownStarted.Count) ` -CountUsersImminent $($ExpiringIminent.Count) ` -CountUsersAlreadyExpired $($UsersExpired.Count) -CountUsersNotified $($UsersNotified.Count) $time.Stop() $FilePathExcel = Get-FileName -Extension 'xlsx' -Temporary if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeSummary.Enabled -eq $true) { $SummaryOfUsers = $Script:UsersApplicable | Group-Object DaysToExpire ` | Select-Object @{Name = 'Days to Expire'; Expression = { [int] $($_.Name) } }, @{Name = 'Users with Days to Expire'; Expression = { [int] $($_.Count) } } $SummaryOfUsers = $SummaryOfUsers | Sort-Object -Property 'Days to Expire' Write-Color @WriteParameters -Text '[i] Preparing data for report ', 'Summary of Expiring Users' -Color White, Yellow if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsHTML -ne $false) { $EmailBody += Set-EmailBody -TableData $SummaryOfUsers ` -TableMessageWelcome "Summary of days to expire and it's count" ` -TableMessageNoData 'There were no users that have days of expiring.' } if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsExcel) { $SummaryOfUsers | ConvertTo-Excel -FilePath $FilePathExcel -ExcelWorkSheetName 'Summary' -AutoFilter -AutoFit } } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludePasswordNotificationsSent.Enabled -eq $true) { Write-Color @WriteParameters -Text '[i] Preparing data for report ', 'Password Notifcations Sent' -Color White, Yellow if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsHTML -ne $false) { $EmailBody += Set-EmailBody -TableData $UsersNotified ` -TableMessageWelcome "Following users had their password notifications sent" ` -TableMessageNoData 'No users required nofifications.' } if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsExcel) { $UsersNotified | ConvertTo-Excel -FilePath $FilePathExcel -ExcelWorkSheetName 'NotificationsSent' -AutoFilter -AutoFit } } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeManagersPasswordNotificationsSent.Enabled -eq $true) { Write-Color @WriteParameters -Text '[i] Preparing data for report ', 'Password Notifcations Sent to Managers' -Color White, Yellow if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsHTML -ne $false) { $EmailBody += Set-EmailBody -TableData $ManagersReceived ` -TableMessageWelcome "Following managers had their password bundle notifications sent" ` -TableMessageNoData 'No managers required nofifications.' } if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsExcel) { $ManagersReceived | ConvertTo-Excel -FilePath $FilePathExcel -ExcelWorkSheetName 'NotificationsSentManagers' -AutoFilter -AutoFit } } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringImminent.Enabled -eq $true) { Write-Color @WriteParameters -Text '[i] Preparing data for report ', 'Users expiring imminent' -Color White, Yellow if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsHTML -ne $false) { $EmailBody += Set-EmailBody -TableData $ExpiringIminent ` -TableMessageWelcome "Following users expiring imminent (Less than $DayLowest day(s)" ` -TableMessageNoData 'No users expiring.' } if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsExcel) { $ExpiringIminent | ConvertTo-Excel -FilePath $FilePathExcel -ExcelWorkSheetName 'ExpiringImminent' -AutoFilter -AutoFit } } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpiringCountdownStarted.Enabled -eq $true) { Write-Color @WriteParameters -Text '[i] Preparing data for report ', 'Expiring Couintdown Started' -Color White, Yellow if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsHTML -ne $false) { $EmailBody += Set-EmailBody -TableData $ExpiringCountdownStarted ` -TableMessageWelcome "Following users expiring countdown started (Less than $DayHighest day(s))" ` -TableMessageNoData 'There were no users that had their coundown started.' } if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsExcel) { $ExpiringCountdownStarted | ConvertTo-Excel -FilePath $FilePathExcel -ExcelWorkSheetName 'ExpiringCountdownStarted' -AutoFilter -AutoFit } } if ($ConfigurationParameters.RemindersSendToAdmins.Reports.IncludeExpired.Enabled -eq $true) { Write-Color @WriteParameters -Text '[i] Preparing data for report ', 'Users are already expired' -Color White, Yellow if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsHTML -ne $false) { if ($ConfigurationParameters.DisableExpiredUsers.Enable -eq $true -and -not $ConfigurationParameters.DisableExpiredUsers.DisplayOnly -eq $true) { $EmailBody += Set-EmailBody -TableData $UsersExpired -TableMessageWelcome "Following users are already expired (and were disabled...)" -TableMessageNoData "No users that are expired." } else { $EmailBody += Set-EmailBody -TableData $UsersExpired -TableMessageWelcome "Following users are already expired (and still enabled...)" -TableMessageNoData "No users that are expired and enabled." } } if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsExcel) { $UsersExpired | ConvertTo-Excel -FilePath $FilePathExcel -ExcelWorkSheetName 'UsersExpired' -AutoFilter -AutoFit } } $EmailBody += "</body>" if ($ConfigurationParameters.Debug.DisplayTemplateHTML -eq $true) { Get-HTML -text $EmailBody } if ($ConfigurationParameters.RemindersSendToAdmins.RemindersDisplayOnly -eq $true) { Write-Color @WriteParameters -Text "[i] Pretending to send email to admins email ", "$($ConfigurationParameters.RemindersSendToAdmins.AdminsEmail) ", "...", 'Success' -Color White, Yellow, White, Green } else { Write-Color @WriteParameters -Text "[i] Sending email to administrators on email address ", "$($ConfigurationParameters.RemindersSendToAdmins.AdminsEmail) ", "..." -Color White, Yellow, White -NoNewLine $EmailSplat = @{ EmailParameters = $EmailParameters Body = $EmailBody Subject = $ConfigurationParameters.RemindersSendToAdmins.AdminsEmailSubject To = $ConfigurationParameters.RemindersSendToAdmins.AdminsEmail } if ($FormattingParameters.CompanyBranding.Inline) { $EmailSplat.InlineAttachments = @{ logo = $FormattingParameters.CompanyBranding.Logo } } if ($ConfigurationParameters.RemindersSendToAdmins.ReportsAsExcel) { $EmailSplat.Attachment = $FilePathExcel } $EmailSent = Send-Email @EmailSplat if ($EmailSent.Status -eq $true) { Write-Color -Text "Done" -Color "Green" -LogFile $WriteParameters.LogFile } else { Write-Color -Text "Failed! Error: $($EmailSent.Error)" -Color "Red" -LogFile $WriteParameters.LogFile } } Write-Color @WriteParameters '[i] Ending processing ', 'Administrators', ' section' -Color White, Yellow, White } else { Write-Color @WriteParameters '[i] Skipping processing ', 'Administrators', ' section' -Color White, Yellow, White } #endregion Send Emails to Admins } Export-ModuleMember -Function @('Find-PasswordExpiryCheck', 'Start-PasswordExpiryCheck') -Alias @() # SIG # Begin signature block # MIIgQAYJKoZIhvcNAQcCoIIgMTCCIC0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUJI6eHvxYTmwTLnqtZ7f81vBw # 5oSgghtvMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0B # AQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg # Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg # +XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT # XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5 # a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g # 0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1 # roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf # GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G # A1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLL # gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3 # cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmr # EthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+ # fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5Q # Z7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu # 838fYxAe+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw # 8jCCBTAwggQYoAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQELBQAw # ZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBS # b290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkGA1UE # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj # ZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUg # U2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjTsxx/ # DhGvZ3cH0wsxSRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3Gde2 # qvCchqXYJawOeSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEoJrsk # acLCUvIUZ4qJRdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCiq85/ # 6XzLkqHlOzEcz+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xnTrXE # 94zRICUj6whkPlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAPZHM8 # np+mM6n9Gd8lk9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD # VR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEBBG0w # azAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF # BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk # SURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRw # Oi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3Js # ME8GA1UdIARIMEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczov # L3d3dy5kaWdpY2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRaxLl7 # KgqjpepxA8Bg+S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823I # DzANBgkqhkiG9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE7zBh # 134LYP3DPQ/Er4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4TS63X # X0R58zYUBor3nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwukaPA # JRHinBRHoXpoaK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2iiQC # /i9yfhzXSUWW6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/GkxAG # /AeB+ova+YJJ92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBT0wggQloAMC # AQICEATV3B9I6snYUgC6zZqbKqcwDQYJKoZIhvcNAQELBQAwcjELMAkGA1UEBhMC # VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0 # LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2ln # bmluZyBDQTAeFw0yMDA2MjYwMDAwMDBaFw0yMzA3MDcxMjAwMDBaMHoxCzAJBgNV # BAYTAlBMMRIwEAYDVQQIDAnFmmzEhXNraWUxETAPBgNVBAcTCEthdG93aWNlMSEw # HwYDVQQKDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMxITAfBgNVBAMMGFByemVt # eXPFgmF3IEvFgnlzIEVWT1RFQzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC # ggEBAL+ygd4sga4ZC1G2xXvasYSijwWKgwapZ69wLaWaZZIlY6YvXTGQnIUnk+Tg # 7EoT7mQiMSaeSPOrn/Im6N74tkvRfQJXxY1cnt3U8//U5grhh/CULdd6M3/Z4h3n # MCq7LQ1YVaa4MYub9F8WOdXO84DANoNVG/t7YotL4vzqZil3S9pHjaidp3kOXGJc # vxrCPAkRFBKvUmYo23QPFa0Rd0qA3bFhn97WWczup1p90y2CkOf28OVOOObv1fNE # EqMpLMx0Yr04/h+LPAAYn6K4YtIu+m3gOhGuNc3B+MybgKePAeFIY4EQzbqvCMy1 # iuHZb6q6ggRyqrJ6xegZga7/gV0CAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrE # uXsqCqOl6nEDwGD5LfZldQ5YMB0GA1UdDgQWBBQYsTUn6BxQICZOCZA0CxS0TZSU # ZjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAw # bjA1oDOgMYYvaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1j # cy1nMS5jcmwwNaAzoDGGL2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFz # c3VyZWQtY3MtZzEuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYB # BQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGE # BggrBgEFBQcBAQR4MHYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0 # LmNvbTBOBggrBgEFBQcwAoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0U0hBMkFzc3VyZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQC # MAAwDQYJKoZIhvcNAQELBQADggEBAJq9bM+JbCwEYuMBtXoNAfH1SRaMLXnLe0py # VK6el0Z1BtPxiNcF4iyHqMNVD4iOrgzLEVzx1Bf/sYycPEnyG8Gr2tnl7u1KGSjY # enX4LIXCZqNEDQCeTyMstNv931421ERByDa0wrz1Wz5lepMeCqXeyiawqOxA9fB/ # 106liR12vL2tzGC62yXrV6WhD6W+s5PpfEY/chuIwVUYXp1AVFI9wi2lg0gaTgP/ # rMfP1wfVvaKWH2Bm/tU5mwpIVIO0wd4A+qOhEia3vn3J2Zz1QDxEprLcLE9e3Gmd # G5+8xEypTR23NavhJvZMgY2kEXBEKEEDaXs0LoPbn6hMcepR2A4wggZqMIIFUqAD # AgECAhADAZoCOv9YsWvW1ermF/BmMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYT # AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2Vy # dC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMTAeFw0xNDEw # MjIwMDAwMDBaFw0yNDEwMjIwMDAwMDBaMEcxCzAJBgNVBAYTAlVTMREwDwYDVQQK # EwhEaWdpQ2VydDElMCMGA1UEAxMcRGlnaUNlcnQgVGltZXN0YW1wIFJlc3BvbmRl # cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNkXfx8s+CCNeDg9sYq # 5kl1O8xu4FOpnx9kWeZ8a39rjJ1V+JLjntVaY1sCSVDZg85vZu7dy4XpX6X51Id0 # iEQ7Gcnl9ZGfxhQ5rCTqqEsskYnMXij0ZLZQt/USs3OWCmejvmGfrvP9Enh1DqZb # FP1FI46GRFV9GIYFjFWHeUhG98oOjafeTl/iqLYtWQJhiGFyGGi5uHzu5uc0LzF3 # gTAfuzYBje8n4/ea8EwxZI3j6/oZh6h+z+yMDDZbesF6uHjHyQYuRhDIjegEYNu8 # c3T6Ttj+qkDxss5wRoPp2kChWTrZFQlXmVYwk/PJYczQCMxr7GJCkawCwO+k8IkR # j3cCAwEAAaOCAzUwggMxMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYG # A1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYDVR0gBIIBtjCCAbIwggGhBglghkgB # hv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20v # Q1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAA # dABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQA # dQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQA # aQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIA # ZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcA # aABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQA # IABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4A # IABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAuMAsGCWCGSAGG/WwDFTAfBgNVHSME # GDAWgBQVABIrE5iymQftHt+ivlcNK2cCzTAdBgNVHQ4EFgQUYVpNJLZJMp1KKnka # g0v0HonByn0wfQYDVR0fBHYwdDA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwOKA2oDSGMmh0dHA6Ly9jcmw0 # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3JsMHcGCCsGAQUF # BwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEG # CCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRB # c3N1cmVkSURDQS0xLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAnSV+GzNNsiaBXJuG # ziMgD4CH5Yj//7HUaiwx7ToXGXEXzakbvFoWOQCd42yE5FpA+94GAYw3+puxnSR+ # /iCkV61bt5qwYCbqaVchXTQvH3Gwg5QZBWs1kBCge5fH9j/n4hFBpr1i2fAnPTgd # KG86Ugnw7HBi02JLsOBzppLA044x2C/jbRcTBu7kA7YUq/OPQ6dxnSHdFMoVXZJB # 2vkPgdGZdA0mxA5/G7X1oPHGdwYoFenYk+VVFvC7Cqsc21xIJ2bIo4sKHOWV2q7E # LlmgYd3a822iYemKC23sEhi991VUQAOSK2vCUcIKSK+w1G7g9BQKOhvjjz3Kr2qN # e9zYRDCCBs0wggW1oAMCAQICEAb9+QOWA63qAArrPye7uhswDQYJKoZIhvcNAQEF # BQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE # CxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJ # RCBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAwMFowYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0xMIIB # IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6IItmfnKwkKVpYBzQHDSnlZU # XKnE0kEGj8kz/E1FkVyBn+0snPgWWd+etSQVwpi5tHdJ3InECtqvy15r7a2wcTHr # zzpADEZNk+yLejYIA6sMNP4YSYL+x8cxSIB8HqIPkg5QycaH6zY/2DDD/6b3+6LN # b3Mj/qxWBZDwMiEWicZwiPkFl32jx0PdAug7Pe2xQaPtP77blUjE7h6z8rwMK5nQ # xl0SQoHhg26Ccz8mSxSQrllmCsSNvtLOBq6thG9IhJtPQLnxTPKvmPv2zkBdXPao # 8S+v7Iki8msYZbHBc63X8djPHgp0XEK4aH631XcKJ1Z8D2KkPzIUYJX9BwSiCQID # AQABo4IDejCCA3YwDgYDVR0PAQH/BAQDAgGGMDsGA1UdJQQ0MDIGCCsGAQUFBwMB # BggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCDCCAdIGA1Ud # IASCAckwggHFMIIBtAYKYIZIAYb9bAABBDCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6 # Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggr # BgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAA # QwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAA # YQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUA # cgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4A # ZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAA # bABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAA # aQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIA # ZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMBIGA1UdEwEB/wQIMAYBAf8C # AQAweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaG # NGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD # QS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz # c3VyZWRJRFJvb3RDQS5jcmwwHQYDVR0OBBYEFBUAEisTmLKZB+0e36K+Vw0rZwLN # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQBGUD7Jtygkpzgdtlspr1LPUukxR6tWXHvVDQtBs+/sdR90OPKyXGGinJXD # UOSCuSPRujqGcq04eKx1XRcXNHJHhZRW0eu7NoR3zCSl8wQZVann4+erYs37iy2Q # wsDStZS9Xk+xBdIOPRqpFFumhjFiqKgz5Js5p8T1zh14dpQlc+Qqq8+cdkvtX8JL # FuRLcEwAiR78xXm8TBJX/l/hHrwCXaj++wc4Tw3GXZG5D2dFzdaD7eeSDY2xaYxP # +1ngIw/Sqq4AfO6cQg7PkdcntxbuD8O9fAqg7iwIVYUiuOsYGk38KiGtSTGDR5V3 # cdyxG0tLHBCcdxTBnU8vWpUIKRAmMYIEOzCCBDcCAQEwgYYwcjELMAkGA1UEBhMC # VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0 # LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2ln # bmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzAJBgUrDgMCGgUAoHgwGAYKKwYBBAGC # NwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUAJEbXc6A # uyg3LIt4B/EGxCQehxYwDQYJKoZIhvcNAQEBBQAEggEALU2LaspPeqF4CsmJidWy # iAE0uPDzrBhPD4vVPVQrRSlfkvr24oatYFqLvHyKV9oGXMT1EDk7OYhYc4QrnYs/ # lp1XQ5Bq/kBZffDtDM1ER/b0JimoCn3CXgwSiLLK4kFKKcPwOON9IscXZg1jAkhI # JI7bQNskCjRsvKi6FE0BkJbOWfpjgPMRGL2+5+XOT4vptaAfiI5upPYA5aKFNcWa # QGnuMyA7dVzfUBgCx+vvIOGTIsHKSgA6Q+zq8sYo2wGKmdPE3Crzzu6VeDalq8de # SAFDtsQNZPbuZy0o85hJvJ43qghFshiazfuhh/mCzUR7fBTEcrF2YiNGR0MFLi5D # RaGCAg8wggILBgkqhkiG9w0BCQYxggH8MIIB+AIBATB2MGIxCzAJBgNVBAYTAlVT # MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j # b20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMQIQAwGaAjr/WLFr # 1tXq5hfwZjAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAc # BgkqhkiG9w0BCQUxDxcNMjAwOTEwMDcxNTQ0WjAjBgkqhkiG9w0BCQQxFgQU9UMX # xiTkimsOaHRWPk99jZxHNd0wDQYJKoZIhvcNAQEBBQAEggEAlAZ+t4LsQR5SwTYJ # Nsxj/mPVIpEUHv79A4twLPlySaJoZSs1vadPfrzmdHOOc0k3e4RHSM3FDy+KBfJu # msRW4KRIoyohlraS29fRtcRRTiwz1xa3/ful/83oEKdWX6glKqfC5BcTFhOa3X5B # AeU0aAuvt0GkaWP4KB8yUp148FIYjU3KNG8NTyKlAI+bEePfZMFsI8Z8dQWZ575a # 38sACCyw7abciyy5w/Qne+seon00FvU0obSG+PVgGKxm99lX5dY4nZAo/9Eu3YVe # mudmKKTiI5gGhnL9pWFKTO2ffn0f/N/O5kYuPEzdtHiDct5/NphHByDehTpG/6Lt # 3XK0ow== # SIG # End signature block |