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() }