Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

194 lines
10KB

  1. param(
  2. [string]$ConfigPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'config.ini'),
  3. [switch]$Send,
  4. [string]$PickupDirectory,
  5. [int]$SessionPid,
  6. [string]$SessionToken,
  7. [string]$TestNowUtc,
  8. [switch]$TestSimulateUnknownAfterSend
  9. )
  10. $ErrorActionPreference = 'Stop'
  11. . (Join-Path $PSScriptRoot 'dispatch-common.ps1')
  12. if ([bool]$Send -eq [bool]$PickupDirectory) { throw 'Choose exactly one mode: -Send or -PickupDirectory' }
  13. if ($Send -and $TestNowUtc) { throw 'TestNowUtc is only available in pickup mode' }
  14. if ($Send -and $TestSimulateUnknownAfterSend) { throw 'Failure injection is only available in pickup mode' }
  15. if ($Send) { Assert-OpenHtaSession $SessionPid $SessionToken }
  16. elseif (-not (Test-Path -LiteralPath $PickupDirectory -PathType Container)) {
  17. throw 'Pickup directory does not exist'
  18. }
  19. $now = if ($TestNowUtc) {
  20. [DateTime]::Parse($TestNowUtc, [cultureinfo]::InvariantCulture,
  21. [System.Globalization.DateTimeStyles]::AdjustToUniversal)
  22. } else { [DateTime]::UtcNow }
  23. $context = Get-MailContext $ConfigPath
  24. $settings = $context.Settings
  25. $interval = Get-QueueInterval $context
  26. if ($settings['SMTP_TLS'] -ne 'on' -or $settings['SMTP_STARTTLS'] -ne 'on') {
  27. throw 'SMTP_TLS and SMTP_STARTTLS must both be on'
  28. }
  29. $port = 0
  30. if (-not [int]::TryParse($settings['SMTP_PORT'], [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
  31. throw 'Invalid SMTP_PORT'
  32. }
  33. $sender = $settings['SMTP_USER'].ToLowerInvariant()
  34. $lock = Enter-DispatchLock
  35. $db = $null
  36. $password = $null
  37. $client = $null
  38. $message = $null
  39. $reservedDelivery = 0
  40. $reservedAttempt = 0
  41. $sendStarted = $false
  42. try {
  43. $db = Open-MailDatabase $context
  44. $state = Get-OperationalState $db
  45. if ($state.Paused) { Write-Output 'Dispatch paused'; return }
  46. Assert-OptOutReviewCurrent $context $state
  47. $clock = Get-DispatchClock $db $sender $interval
  48. if ($null -ne $clock.Next -and $now -lt $clock.Next) {
  49. Write-Output ('Next attempt allowed at ' + $clock.Next.ToString('o'))
  50. return
  51. }
  52. if ($Send) { $password = Load-SmtpPassword $context }
  53. $transaction = $db.BeginTransaction()
  54. try {
  55. while ($true) {
  56. $sql = "SELECT TOP 1 d.[Id],d.[EmailKey] FROM [Delivery] AS d INNER JOIN [Campaign] AS m ON d.[CampaignId]=m.[Id] WHERE d.[Status]='pending' AND m.[Status]='running' AND m.[StartAtUtc]<=? ORDER BY d.[Id]"
  57. $pick = New-DbCommand $db $sql $transaction
  58. $null = $pick.Parameters.Add('now', [System.Data.OleDb.OleDbType]::Date)
  59. $pick.Parameters[0].Value = $now
  60. $reader = $pick.ExecuteReader()
  61. try {
  62. if (-not $reader.Read()) { $candidate = $null }
  63. else { $candidate = @{ Id = [int]$reader.GetValue(0); Key = [string]$reader.GetValue(1) } }
  64. }
  65. finally { $reader.Dispose() }
  66. if ($null -eq $candidate) { break }
  67. if (-not (Test-Suppressed $db $candidate.Key $transaction)) { break }
  68. $skip = New-DbCommand $db "UPDATE [Delivery] SET [Status]='suppressed' WHERE [Id]=? AND [Status]='pending'" $transaction
  69. $null = $skip.Parameters.Add('id', [System.Data.OleDb.OleDbType]::Integer)
  70. $skip.Parameters[0].Value = $candidate.Id
  71. $null = $skip.ExecuteNonQuery()
  72. }
  73. if ($null -eq $candidate) {
  74. $transaction.Commit()
  75. Complete-FinishedCampaigns $db
  76. Write-Output 'No pending delivery is due'
  77. return
  78. }
  79. $delivery = New-DbCommand $db "UPDATE [Delivery] SET [Status]='in_flight',[AttemptCount]=[AttemptCount]+1,[LastAttemptAtUtc]=? WHERE [Id]=? AND [Status]='pending'" $transaction
  80. $null = $delivery.Parameters.Add('now', [System.Data.OleDb.OleDbType]::Date)
  81. $delivery.Parameters[0].Value = $now
  82. $null = $delivery.Parameters.Add('id', [System.Data.OleDb.OleDbType]::Integer)
  83. $delivery.Parameters[1].Value = $candidate.Id
  84. if ($delivery.ExecuteNonQuery() -ne 1) { throw 'Delivery was already reserved' }
  85. $attempt = New-DbCommand $db 'INSERT INTO [Attempt] ([DeliveryId],[StartedAtUtc],[Outcome]) VALUES (?,?,?)' $transaction
  86. $null = $attempt.Parameters.Add('delivery', [System.Data.OleDb.OleDbType]::Integer)
  87. $attempt.Parameters[0].Value = $candidate.Id
  88. $null = $attempt.Parameters.Add('now', [System.Data.OleDb.OleDbType]::Date)
  89. $attempt.Parameters[1].Value = $now
  90. Add-TextParameter $attempt 'outcome' 30 'started'
  91. $null = $attempt.ExecuteNonQuery()
  92. $identity = New-DbCommand $db 'SELECT @@IDENTITY' $transaction
  93. $reservedAttempt = [int]$identity.ExecuteScalar()
  94. $next = $now.AddSeconds($interval)
  95. if ($clock.Exists) {
  96. $pace = New-DbCommand $db 'UPDATE [DispatchState] SET [LastAttemptAtUtc]=?,[NextAllowedAttemptUtc]=? WHERE [SenderKey]=?' $transaction
  97. $null = $pace.Parameters.Add('last', [System.Data.OleDb.OleDbType]::Date)
  98. $pace.Parameters[0].Value = $now
  99. $null = $pace.Parameters.Add('next', [System.Data.OleDb.OleDbType]::Date)
  100. $pace.Parameters[1].Value = $next
  101. Add-TextParameter $pace 'sender' 254 $sender
  102. }
  103. else {
  104. $pace = New-DbCommand $db 'INSERT INTO [DispatchState] ([SenderKey],[LastAttemptAtUtc],[NextAllowedAttemptUtc]) VALUES (?,?,?)' $transaction
  105. Add-TextParameter $pace 'sender' 254 $sender
  106. $null = $pace.Parameters.Add('last', [System.Data.OleDb.OleDbType]::Date)
  107. $pace.Parameters[1].Value = $now
  108. $null = $pace.Parameters.Add('next', [System.Data.OleDb.OleDbType]::Date)
  109. $pace.Parameters[2].Value = $next
  110. }
  111. $null = $pace.ExecuteNonQuery()
  112. $reservedDelivery = $candidate.Id
  113. $transaction.Commit()
  114. }
  115. catch { $transaction.Rollback(); throw }
  116. try {
  117. if ($Send) { Assert-OpenHtaSession $SessionPid $SessionToken }
  118. $snapshot = Get-DeliverySnapshot $db $reservedDelivery
  119. if ($snapshot.CampaignStatus -ne 'running') { throw 'Campaign was paused or canceled before send' }
  120. if (Test-Suppressed $db $snapshot.EmailKey) {
  121. Complete-DeliveryAttempt $db $reservedDelivery $reservedAttempt 'skipped' 'suppressed' 'suppressed_before_send'
  122. Write-Output "Delivery $reservedDelivery skipped because it is suppressed"
  123. return
  124. }
  125. $template = Get-TemplateSnapshot $db $snapshot.TemplateId
  126. $html = Add-CampaignFooter $template.Document @{
  127. From = $snapshot.From; ReplyTo = $snapshot.ReplyTo; Postal = $snapshot.Postal }
  128. $message = New-Object System.Net.Mail.MailMessage
  129. $message.From = New-Object System.Net.Mail.MailAddress($snapshot.From)
  130. $null = $message.ReplyToList.Add($snapshot.ReplyTo)
  131. $null = $message.To.Add($snapshot.Email)
  132. $message.Subject = $snapshot.Subject
  133. $message.SubjectEncoding = [System.Text.Encoding]::UTF8
  134. $view = [System.Net.Mail.AlternateView]::CreateAlternateViewFromString(
  135. $html, [System.Text.Encoding]::UTF8, 'text/html')
  136. foreach ($image in $template.Images) {
  137. $stream = New-Object System.IO.MemoryStream -ArgumentList (,([byte[]]$image.Bytes))
  138. $resource = New-Object System.Net.Mail.LinkedResource($stream, $image.Type)
  139. $resource.ContentId = $image.Cid
  140. $resource.TransferEncoding = [System.Net.Mime.TransferEncoding]::Base64
  141. $null = $view.LinkedResources.Add($resource)
  142. }
  143. $null = $message.AlternateViews.Add($view)
  144. $client = New-Object System.Net.Mail.SmtpClient($settings['SMTP_HOST'], $port)
  145. $client.Timeout = 20000
  146. if ($Send) {
  147. $client.UseDefaultCredentials = $false
  148. $client.Credentials = New-Object System.Net.NetworkCredential -ArgumentList @($settings['SMTP_USER'], $password)
  149. $client.EnableSsl = $true
  150. Assert-OpenHtaSession $SessionPid $SessionToken
  151. if ((Get-OperationalState $db).Paused -or (Test-Suppressed $db $snapshot.EmailKey)) {
  152. throw 'Campaign paused or recipient suppressed before SMTP call'
  153. }
  154. }
  155. else {
  156. $client.DeliveryMethod = [System.Net.Mail.SmtpDeliveryMethod]::SpecifiedPickupDirectory
  157. $client.PickupDirectoryLocation = [System.IO.Path]::GetFullPath($PickupDirectory)
  158. }
  159. $sendStarted = $true
  160. $client.Send($message)
  161. if ($TestSimulateUnknownAfterSend) { throw 'Injected loss of provider acknowledgement' }
  162. Complete-DeliveryAttempt $db $reservedDelivery $reservedAttempt 'accepted' 'accepted'
  163. Complete-FinishedCampaigns $db
  164. Write-Output "SMTP accepted delivery $reservedDelivery. Next slot at $($next.ToString('o'))."
  165. if ($Send) { $sentNote = Save-SentCopySafely $settings $password $message; if ($sentNote) { Write-Output $sentNote } }
  166. }
  167. catch {
  168. $code = if ($_.Exception -is [System.Net.Mail.SmtpException]) {
  169. 'smtp_' + $_.Exception.StatusCode.ToString()
  170. } elseif ($sendStarted) { 'provider_outcome_unknown' } else { 'before_send_error' }
  171. if ($sendStarted) {
  172. Complete-DeliveryAttempt $db $reservedDelivery $reservedAttempt 'uncertain' 'uncertain' $code
  173. Pause-DispatchForReview $db $reservedDelivery
  174. Write-Output "Delivery $reservedDelivery has uncertain provider outcome ($code). All campaigns paused for manual review."
  175. }
  176. elseif ($_.Exception.Message -match 'HTA session|Campaign was paused') {
  177. Complete-DeliveryAttempt $db $reservedDelivery $reservedAttempt 'aborted' 'pending' 'session_closed_before_send'
  178. Write-Output "Delivery $reservedDelivery was not sent because the HTA session closed or paused. It remains pending."
  179. }
  180. else {
  181. Complete-DeliveryAttempt $db $reservedDelivery $reservedAttempt 'failed' 'failed' $code
  182. Write-Output "Delivery $reservedDelivery failed before SMTP ($code)."
  183. }
  184. }
  185. }
  186. finally {
  187. if ($client) { $client.Dispose() }
  188. if ($message) { $message.Dispose() }
  189. if ($password) { $password.Dispose() }
  190. if ($db) { $db.Dispose() }
  191. $lock.Dispose()
  192. }

Powered by TurnKey Linux.