Import-Module PSScheduledJob New-Variable -Name 'EncryptionKey' -Value (xxxx) -Option 'Constant' -Scope 'Script' -ErrorAction 'SilentlyContinue' New-Variable -Name 'Credential' -Scope 'Script' -Force New-Variable -Name 'ScriptDirectory' -Scope 'Script' -Force New-Variable -Name 'XMLSettings' -Scope 'Script' -Force New-Variable -Name 'XMLAllEmails' -Scope 'Script' -Force New-Variable -Name 'XMLOutcomeEmail' -Scope 'Script' -Force New-Variable -Name 'XMLErrorEmail' -Scope 'Script' -Force New-Variable -Name 'XMLDelays' -Scope 'Script' -Force Function Get-ScriptDirectory { [OutputType([string])] Param () If ($null -ne $hostinvocation) { Split-Path $hostinvocation.MyCommand.path } Else { Split-Path $script:MyInvocation.MyCommand.Path } } Function Read-ConfigurationFile { [CmdletBinding()] Param () Try { $Path = "$Script:ScriptDirectory\TheExecutioner.xml" If (Test-Path -Path $Path) { [XML]$XMLConfigurationFile = [XML](Get-Content -Path $Path -Encoding 'UTF8') $Script:XMLSettings = $XMLConfigurationFile.SelectNodes("/TheExecutioner/Settings") $Script:XMLAllEmails = $XMLConfigurationFile.SelectNodes("/TheExecutioner/AllEmails") $Script:XMLOutcomeEmail = $XMLConfigurationFile.SelectNodes("/TheExecutioner/OutcomeEmail") $Script:XMLErrorEmail = $XMLConfigurationFile.SelectNodes("/TheExecutioner/ErrorEmail") $Script:XMLDelays = $XMLConfigurationFile.SelectNodes("/TheExecutioner/Delays") $ReadConfigurationFile = $True } Else { Throw 'TheExecutioner.xml not found error.' } } Catch { Throw } } Function Read-Requests { [CmdletBinding()][OutputType([System.Management.Automation.PSObject])] $Requests = @() $Files = Get-ChildItem -Path '.\Requests' -Filter '*.txt' ForEach ($File In $Files) { $Name = $File.BaseName $TicketNumber = '' $sAMAccountName = '' $DateAndTime = '' $Emails = @() $ErrorLog = @() $Lines = Get-Content -Path $File.FullName ForEach ($Line In $Lines) { If ($Line -notmatch '^#') { Switch -regex ($Line) { '(?i)^Ticket number:.+$' { Try { $TicketNumber = Get-TicketNumber -TicketNumberLine $Line } Catch { $ErrorLog += $_ } Break } '(?i)^sAMAccountName:.+$' { Try { $sAMAccountName = Get-sAMAccountName -sAMAccountNameLine $Line } Catch { $ErrorLog += $_ } Break } '(?i)^Date and time:.+$' { Try { $DateAndTime = Get-DateAndTime -DateAndTimeLine $Line } Catch { $ErrorLog += $_ } Break } '(?i)^Email:.+$' { Try { $Emails = Get-Emails -EmailsLine $Line } Catch { $ErrorLog += $_ } Break } Default { $ErrorLog += "Invalid line `"$Line`"." } } } } If ($TicketNumber -and $sAMAccountName -and $DateAndTime -and $Emails -and -not ($ErrorLog)) { $Requests += New-Request -Name $Name -TicketNumber $TicketNumber -sAMAccountName $sAMAccountName -DateAndTime $DateAndTime -Emails $Emails } ElseIf ($ErrorLog) { $Requests += New-Request -Name $Name -ErrorLog $ErrorLog } } Return $Requests } Function Get-TicketNumber { [CmdletBinding()][OutputType([String])] Param ( [Parameter(Mandatory = $true)][String]$TicketNumberLine ) Try { $TicketNumber = ($TicketNumberLine.Split(':', 2)[1]).Trim() If ($TicketNumber -notmatch '^.+$') { Throw 'Ticket number format error.' } } Catch { $TicketNumber = '' Throw } Return $TicketNumber } Function Get-sAMAccountName { [CmdletBinding()][OutputType([String])] Param ( [Parameter(Mandatory = $true)][String]$sAMAccountNameLine ) Try { $sAMAccountName = ($sAMAccountNameLine.Split(':', 2)[1]).Trim() If ($sAMAccountName -notmatch '^.+$') { Throw 'sAMAccountName format error.' } If (-not ([ADSISearcher]"sAMAccountName=$sAMAccountName").FindOne()) { Throw 'User not found in Active Directory error.' } } Catch { $sAMAccountName = '' Throw } Return $sAMAccountName } Function Get-DateAndTime { [CmdletBinding()][OutputType([DateTime])] Param ( [Parameter(Mandatory = $true)][String]$DateAndTimeLine ) Try { $DateAndTime = ($DateAndTimeLine.Split(':', 2)[1]).Trim() If ($DateAndTime -notmatch '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$') { Throw 'Date and time format error.' } If (-not ([DateTime]::TryParse($DateAndTime, [Ref]$DateAndTime))) { Throw 'Date and time invalid error.' } } Catch { $DateAndTime = '' Throw } Return $DateAndTime } Function Get-Emails { [CmdletBinding()][OutputType([String])] Param ( [Parameter(Mandatory = $true)][String]$EmailsLine ) $Emails = @() Try { $EmailsString = ($EmailsLine.Split(':', 2)[1]).Trim() If ($EmailsString -notmatch '^.+$') { Throw 'Email format error.' } ForEach ($EmailString In $EmailsString.Split(';')) { If ($EmailString -notmatch '^[_a-z0-9-]+(.[a-z0-9-]+)@[a-z0-9-]+(.[a-z0-9-]+)*(\.[a-z]{2,4})$') { Throw 'Email invalid error.' } Else { $Emails += $EmailString } } } Catch { $Emails = @() Throw } Return $Emails } Function New-Request { [CmdletBinding(DefaultParameterSetName = 'Correct')] Param ( [Parameter(ParameterSetName = 'Correct', Mandatory = $true)][Parameter(ParameterSetName = 'Incorrect', Mandatory = $true)][String]$Name, [Parameter(ParameterSetName = 'Correct', Mandatory = $true)][String]$TicketNumber, [Parameter(ParameterSetName = 'Correct', Mandatory = $true)][String]$sAMAccountName, [Parameter(ParameterSetName = 'Correct', Mandatory = $true)][DateTime]$DateAndTime, [Parameter(ParameterSetName = 'Correct', Mandatory = $true)][String[]]$Emails, [Parameter(ParameterSetName = 'Incorrect', Mandatory = $true)][String[]]$ErrorLog ) $Request = New-Object –TypeName 'System.Management.Automation.PSObject' $Request | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $Name $Request | Add-Member -MemberType 'NoteProperty' -Name 'TicketNumber' -Value '' $Request | Add-Member -MemberType 'NoteProperty' -Name 'sAMAccountName' -Value '' $Request | Add-Member -MemberType 'NoteProperty' -Name 'DateAndTime' -Value '' $Request | Add-Member -MemberType 'NoteProperty' -Name 'Emails' -Value '' $Request | Add-Member -MemberType 'NoteProperty' -Name 'Log' -Value @() $Request | Add-Member -MemberType 'NoteProperty' -Name 'ErrorLog' -Value @() $Request | Add-Member -MemberType 'NoteProperty' -Name 'Outcome' -Value $Null If ($PSCmdlet.ParameterSetName -eq 'Correct') { $Request.TicketNumber = $TicketNumber $Request.sAMAccountName = $sAMAccountName $Request.DateAndTime = $DateAndTime $Request.Emails = $Emails } ElseIf ($PSCmdlet.ParameterSetName -eq 'Incorrect') { $Request.ErrorLog = $ErrorLog } $Request.Log += "Creation time: $(Get-Date)" Return $Request } Function Get-DisableADUserScriptBlock { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)][String]$sAMAccountName, [Parameter(Mandatory = $false)][String]$WindowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, [Parameter(Mandatory = $true)][String]$TicketNumber, [Parameter(Mandatory = $true)][DateTime]$DateAndTime ) $ScriptBlocksAMAccountName = $sAMAccountName.Replace('''', '''''') $ScriptBlockWindowsIdentity = $WindowsIdentity.Replace('''', '''''') $ScriptBlockTicketNumber = $TicketNumber.Replace('''', '''''') $ScriptBlockDateAndTime = $DateAndTime.ToString('yyyy-MM-dd HH:mm') $DisableADUserScriptBlock = @" Try { Import-Module -Name 'ActiveDirectory' -ErrorAction 'Stop' | Out-Null `$ADUser = Get-ADUser -Identity '$ScriptBlocksAMAccountName' -ErrorAction 'Stop' `$ADUser | Disable-ADAccount -Confirm:`$False -ErrorAction 'Stop' | Out-Null `$ADUser | Set-ADUser -Description 'Disabled by $ScriptBlockWindowsIdentity on $ScriptBlockDateAndTime - Ticket #$ScriptBlockTicketNumber' -Confirm:`$False -ErrorAction 'Stop' | Out-Null Return 0 } Catch { Throw } "@ Return [ScriptBlock]::Create($DisableADUserScriptBlock) } Function Send-RequestEmail { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [PSObject]$Request, [String[]]$To = @(), [String]$Subject, [String]$Body, [String[]]$Bcc = @(), [String[]]$Cc = @(), [String]$Priority = 'Normal' ) Try { $To += $Request.Emails $To = $To | Where-Object -FilterScript { $_ -ne '' } $From = $XMLAllEmails.From $SmtpServer = $XMLAllEmails.SmtpServer $Subject = Merge-EmailTemplateWithRequest -EmailTemplate $Subject -Request $Request $Body = Merge-EmailTemplateWithRequest -EmailTemplate $Body -Request $Request $Bcc += $(($XMLAllEmails.Bcc).Split(';')) $Bcc = $Bcc | Where-Object -FilterScript { $_ -ne '' } $Cc += $(($XMLAllEmails.Cc).Split(';')) $Cc = $Cc | Where-Object -FilterScript { $_ -ne '' } Send-MailMessage -From $XMLAllEmails.From -To $To -Subject $Subject -Body $Body -SmtpServer $XMLAllEmails.SmtpServer -Bcc $Bcc -Cc $Cc -Priority $Priority -BodyAsHtml } Catch { Throw } } Function Merge-EmailTemplateWithRequest { [CmdletBinding()][OutputType([string])] Param ( [Parameter(Mandatory = $true)][String]$EmailTemplate, [Parameter(Mandatory = $true)][PSObject]$Request ) Try { ForEach ($Property In $Request | Get-Member -MemberType NoteProperty) { $EmailTemplate = $EmailTemplate.Replace("%$($Property.Name)%", $Request.$($Property.Name)) } } Catch { Throw } Return $EmailTemplate } $ErrorActionPreference = 'Stop' Try { $Script:ScriptDirectory = Get-ScriptDirectory If (Test-Path -Path $(Join-Path -Path $Script:ScriptDirectory -ChildPath '.\TheExecutioner.error.log')) { Remove-Item -Path $(Join-Path -Path $Script:ScriptDirectory -ChildPath '.\TheExecutioner.error.log') -Force -Confirm:$False } Read-ConfigurationFile $Credential = New-Object -Typename 'System.Management.Automation.PSCredential' -Argumentlist $XMLSettings.AccountName, (ConvertTo-SecureString -String $XMLSettings.EncryptedPassword -Key $EncryptionKey) While ($True) { $Requests = Read-Requests #Scheduling new requests and gathering the outcome of completed requests ForEach ($Request In $Requests | Where-Object -FilterScript { -not ($_.ErrorLog) }) { Try { $ScheduledJob = Get-ScheduledJob -Name $Request.Name -ErrorAction 'Stop' If ($(Get-Date).AddMinutes(-1) -gt $(Get-Date -Date $ScheduledJob.JobTriggers.At)) { $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Job should be running or completed.' Try { $Job = Get-Job -Name $Request.Name -ErrorAction 'Stop' If ($Job.State -eq 'Running') { $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Job found in ''Running'' state. Allowing one minute to complete.' $Job | Wait-Job -Timeout 60 } If ($Job.State -eq 'Completed') { $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Job found in ''Completed'' state.' $Request.Outcome = $Job | Receive-Job -ErrorAction 'Stop' $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Received job outcome.' If (Test-Path -Path ".\Requests\$($Request.Name).txt.completed") { Remove-Item -Path ".\Requests\$($Request.Name).txt.completed" -Force -Confirm:$False } Rename-Item -Path ".\Requests\$($Request.Name).txt" -NewName "$($Request.Name).txt.completed" -Confirm:$False -Force -ErrorAction 'Stop' $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Renamed request file.' } Else { $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Job hasn''t been found in ''Completed'' state. Throwing an error.' Throw "Job has been found in the '$($Job.State)' state." } } Catch [System.Management.Automation.PSArgumentException] { $Request.ErrorLog += 'Unable to receive the outcome of the job.' } Catch { $Request.ErrorLog += $_ } Finally { $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Unregistering scheduled job.' Unregister-ScheduledJob -Name $Request.Name } } Else { $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - Job is scheduled to run at a later time.' } } Catch [System.Management.Automation.RuntimeException] { If ($Request.DateAndTime -lt (Get-Date).AddMinutes($Script:XMLDelays.MinimumTimeMinutes)) { $Request.ErrorLog += 'Date and time too close from current time to be processed.' } Else { $Request.Log += $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' - A new scheduled job will be scheduled as requested.' $JobTrigger = New-JobTrigger -Once -At $Request.DateAndTime -RandomDelay $(New-TimeSpan -Seconds $Script:XMLDelays.RandomDelaySeconds) $ScheduledJobOption = New-ScheduledJobOption -HideInTaskScheduler -DoNotAllowDemandStart -WakeToRun -RequireNetwork Register-ScheduledJob -Credential $Credential -Name $Request.Name -ScriptBlock $(Get-DisableADUserScriptBlock -sAMAccountName $Request.sAMAccountName -TicketNumber $Request.TicketNumber -DateAndTime $Request.DateAndTime) -Trigger $JobTrigger -ScheduledJobOption $ScheduledJobOption | Out-Null } } Catch { $Request.ErrorLog += $_ } If ($Script:XMLSettings.LogLevel -eq 'Verbose') { $Request.Log | Out-File -FilePath $(Join-Path -Path $Script:XMLSettings.LogsFolder -ChildPath "$($Request.Name).$(Get-Date -Format 'yyyy-MM-dd HHmmss').log") -Force } } #Validating the outcome of completed jobs ForEach ($Request In $Requests | Where-Object -FilterScript { $_.Outcome -ne $Null }) { If ($Request.Outcome -eq 0) { If (-not (([ADSISearcher]"sAMAccountName=$($Request.sAMAccountName)").FindOne()).Properties.useraccountcontrol -band 0x2) { $Request.ErrorLog += 'Job has been executed without error but user is found still enabled in AD.' } Else { Send-RequestEmail -Request $Request -Bcc $XMLOutcomeEmail.Bcc -Body $XMLOutcomeEmail.Body -Cc $XMLOutcomeEmail.Cc -Priority $XMLOutcomeEmail.Priority -Subject $XMLOutcomeEmail.Subject } } Else { $Request.ErrorLog += 'Unexpected job outcome.' } } #Logging errors ForEach ($Request In $Requests | Where-Object -FilterScript { $_.ErrorLog }) { Send-RequestEmail -Request $Request -To $($XMLErrorEmail.To).Split(';') -Bcc $XMLErrorEmail.Bcc -Body $XMLErrorEmail.Body -Cc $XMLErrorEmail.Cc -Priority $XMLErrorEmail.Priority -Subject $XMLErrorEmail.Subject $Request.ErrorLog | Out-File -FilePath $(Join-Path -Path $Script:XMLSettings.LogsFolder -ChildPath "$($Request.Name).$(Get-Date -Format 'yyyy-MM-dd HHmmss').error.log") -Force If (Test-Path -Path $(Join-Path -Path $Script:XMLSettings.RequestsFolder -ChildPath "$($Request.Name).txt.bad")) { Remove-Item -Path $(Join-Path -Path $Script:XMLSettings.RequestsFolder -ChildPath "$($Request.Name).txt.bad") -Force -Confirm:$False } Rename-Item -Path $(Join-Path -Path $Script:XMLSettings.RequestsFolder -ChildPath "$($Request.Name).txt") -NewName "$($Request.Name).txt.bad" -Confirm:$False -Force -ErrorAction 'Stop' } Start-Sleep -Seconds $Script:XMLDelays.LoopTimeSeconds } } Catch { $_ | Out-File -FilePath $(Join-Path -Path $Script:ScriptDirectory -ChildPath '.\TheExecutioner.error.log') -Force }