|
- param(
- [Parameter(Mandatory = $true)][ValidateSet('Recover','ReviewOptOut','Resume','PauseAll','Cancel','ResolveAccepted','AuthorizeRetry','Status')][string]$Action,
- [int]$CampaignId,
- [int]$DeliveryId,
- [string]$ConfigPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'config.ini')
- )
- $ErrorActionPreference = 'Stop'
- . (Join-Path $PSScriptRoot 'dispatch-common.ps1')
- $context = Get-MailContext $ConfigPath
- $lock = Enter-DispatchLock -WaitMilliseconds 30000
- $db = $null
- try {
- $db = Open-MailDatabase $context
- if ($Action -eq 'Status') {
- $state = Get-OperationalState $db
- $command = New-DbCommand $db "SELECT COUNT(*) FROM [Campaign] WHERE [Status]='running'"
- $running = [int]$command.ExecuteScalar()
- $counts = @{}
- foreach ($status in @('pending','in_flight','accepted','uncertain','suppressed','canceled')) {
- $command = New-DbCommand $db 'SELECT COUNT(*) FROM [Delivery] WHERE [Status]=?'
- Add-TextParameter $command 'status' 30 $status
- $counts[$status] = [int]$command.ExecuteScalar()
- }
- $clock = Get-DispatchClock $db $context.Settings['SMTP_USER'].ToLowerInvariant() (Get-QueueInterval $context)
- $nextText = if ($null -eq $clock.Next) { $null } else { $clock.Next.ToString('o') }
- $reviewText = if ($null -eq $state.Reviewed) { $null } else { $state.Reviewed.ToString('o') }
- [pscustomobject]@{ Paused = $state.Paused; RunningCampaigns = $running;
- Counts = $counts; NextAllowedAttemptUtc = $nextText;
- LastOptOutReviewUtc = $reviewText } | ConvertTo-Json -Compress -Depth 4
- return
- }
- $transaction = $db.BeginTransaction()
- try {
- if ($Action -eq 'Recover') {
- $command = New-DbCommand $db "UPDATE [Campaign] SET [Status]='approved_paused' WHERE [Status]='running'" $transaction
- $null = $command.ExecuteNonQuery()
- $command = New-DbCommand $db 'UPDATE [OperationalState] SET [GlobalPause]=TRUE WHERE [Id]=1' $transaction
- $null = $command.ExecuteNonQuery()
- $command = New-DbCommand $db "UPDATE [Attempt] SET [Outcome]='uncertain',[EndedAtUtc]=?,[SanitizedError]='HTA session ended before provider outcome was recorded' WHERE [Outcome]='started'" $transaction
- $null = $command.Parameters.Add('ended', [System.Data.OleDb.OleDbType]::Date)
- $command.Parameters[0].Value = [DateTime]::UtcNow
- $null = $command.ExecuteNonQuery()
- $command = New-DbCommand $db "UPDATE [Delivery] SET [Status]='uncertain',[LastErrorCode]='session_interrupted' WHERE [Status]='in_flight'" $transaction
- $null = $command.ExecuteNonQuery()
- Write-CampaignAudit $db 'dispatcher_recovered' 'session' 'Open-window dispatcher started paused' $transaction
- $message = 'Recovered previous session; all campaigns are paused.'
- }
- elseif ($Action -eq 'ReviewOptOut') {
- $command = New-DbCommand $db 'UPDATE [OperationalState] SET [LastOptOutReviewAtUtc]=? WHERE [Id]=1' $transaction
- $null = $command.Parameters.Add('reviewed', [System.Data.OleDb.OleDbType]::Date)
- $command.Parameters[0].Value = [DateTime]::UtcNow
- $null = $command.ExecuteNonQuery()
- Write-CampaignAudit $db 'opt_out_reviewed' 'mailbox' 'Operator confirmed reply mailbox review' $transaction
- $message = 'Reply mailbox review recorded.'
- }
- elseif ($Action -eq 'PauseAll') {
- $command = New-DbCommand $db "UPDATE [Campaign] SET [Status]='approved_paused' WHERE [Status]='running'" $transaction
- $null = $command.ExecuteNonQuery()
- $command = New-DbCommand $db 'UPDATE [OperationalState] SET [GlobalPause]=TRUE WHERE [Id]=1' $transaction
- $null = $command.ExecuteNonQuery()
- Write-CampaignAudit $db 'dispatch_paused' 'all' 'Operator paused all campaigns' $transaction
- $message = 'All campaigns paused. A current in-flight attempt may finish.'
- }
- elseif ($Action -eq 'Resume') {
- if ($CampaignId -lt 1) { throw 'Select a campaign to resume' }
- $null = Get-CampaignIdentity $context
- $null = Get-QueueInterval $context
- $state = Get-OperationalState $db $transaction
- Assert-OptOutReviewCurrent $context $state
- $command = New-DbCommand $db "SELECT COUNT(*) FROM [Delivery] WHERE [Status]='uncertain'" $transaction
- if ([int]$command.ExecuteScalar() -gt 0) {
- throw 'Resolve uncertain deliveries before resuming any campaign'
- }
- $command = New-DbCommand $db "SELECT COUNT(*) FROM [Delivery] WHERE [CampaignId]=? AND [Status]='pending'" $transaction
- $null = $command.Parameters.Add('id', [System.Data.OleDb.OleDbType]::Integer)
- $command.Parameters[0].Value = $CampaignId
- if ([int]$command.ExecuteScalar() -lt 1) { throw 'Campaign has no pending deliveries' }
- $command = New-DbCommand $db "UPDATE [Campaign] SET [Status]='running' WHERE [Id]=? AND [Status]='approved_paused'" $transaction
- $null = $command.Parameters.Add('id', [System.Data.OleDb.OleDbType]::Integer)
- $command.Parameters[0].Value = $CampaignId
- if ($command.ExecuteNonQuery() -ne 1) { throw 'Campaign is not paused or does not exist' }
- $command = New-DbCommand $db 'UPDATE [OperationalState] SET [GlobalPause]=FALSE WHERE [Id]=1' $transaction
- $null = $command.ExecuteNonQuery()
- Write-CampaignAudit $db 'campaign_resumed' ([string]$CampaignId) 'Operator explicitly resumed campaign' $transaction
- $message = "Campaign $CampaignId resumed. Dispatch runs only while this HTA is open."
- }
- elseif ($Action -eq 'Cancel') {
- if ($CampaignId -lt 1) { throw 'Select a campaign to cancel' }
- $command = New-DbCommand $db "UPDATE [Campaign] SET [Status]='canceled' WHERE [Id]=? AND [Status] IN ('approved_paused','running')" $transaction
- $null = $command.Parameters.Add('id', [System.Data.OleDb.OleDbType]::Integer)
- $command.Parameters[0].Value = $CampaignId
- if ($command.ExecuteNonQuery() -ne 1) { throw 'Campaign is not active or does not exist' }
- $command = New-DbCommand $db "UPDATE [Delivery] SET [Status]='canceled' WHERE [CampaignId]=? AND [Status]='pending'" $transaction
- $null = $command.Parameters.Add('id', [System.Data.OleDb.OleDbType]::Integer)
- $command.Parameters[0].Value = $CampaignId
- $null = $command.ExecuteNonQuery()
- Write-CampaignAudit $db 'campaign_canceled' ([string]$CampaignId) 'Operator canceled pending deliveries' $transaction
- $message = "Campaign $CampaignId canceled. A current in-flight attempt may finish."
- }
- elseif ($Action -in @('ResolveAccepted','AuthorizeRetry')) {
- if ($DeliveryId -lt 1) { throw 'Select an uncertain delivery' }
- $state = Get-OperationalState $db $transaction
- if (-not $state.Paused) { throw 'Pause all campaigns before resolving an uncertain delivery' }
- if ($Action -eq 'AuthorizeRetry') {
- $command = New-DbCommand $db "UPDATE [Delivery] SET [Status]='pending',[LastErrorCode]='manual_retry_authorized' WHERE [Id]=? AND [Status]='uncertain' AND [AttemptCount]<3" $transaction
- }
- else {
- $command = New-DbCommand $db "UPDATE [Delivery] SET [Status]='accepted',[LastErrorCode]='operator_verified' WHERE [Id]=? AND [Status]='uncertain'" $transaction
- }
- $null = $command.Parameters.Add('id', [System.Data.OleDb.OleDbType]::Integer)
- $command.Parameters[0].Value = $DeliveryId
- if ($command.ExecuteNonQuery() -ne 1) {
- throw 'Delivery is not uncertain, or the three-attempt manual retry limit was reached'
- }
- $auditAction = if ($Action -eq 'AuthorizeRetry') { 'retry_authorized' } else { 'delivery_verified_accepted' }
- Write-CampaignAudit $db $auditAction ([string]$DeliveryId) 'Operator reconciled uncertain provider result' $transaction
- $message = if ($Action -eq 'AuthorizeRetry') {
- "Delivery $DeliveryId is pending for one explicitly authorized retry. Resume the campaign when ready."
- } else { "Delivery $DeliveryId marked accepted after operator verification." }
- }
- $transaction.Commit()
- Write-Output $message
- }
- catch { $transaction.Rollback(); throw }
- }
- finally {
- if ($db) { $db.Dispose() }
- $lock.Dispose()
- }
|